typescript 43 lines · 7 steps

Building a configurable HTML sanitizer allowlist

A whitelist-driven wrapper around sanitize-html that opts into links and images while hardening every anchor tag.

Explained by highlit
1import sanitizeHtml from "sanitize-html";
2 
3interface RichTextOptions {
4 allowImages?: boolean;
5 allowLinks?: boolean;
6}
7 
8const BASE_TAGS = ["p", "br", "strong", "em", "u", "ul", "ol", "li", "blockquote", "code", "pre", "h2", "h3"];
9 
10export function sanitizeRichText(dirty: string, opts: RichTextOptions = {}): string {
11 const allowedTags = [...BASE_TAGS];
12 const allowedAttributes: sanitizeHtml.IOptions["allowedAttributes"] = {};
13 
14 if (opts.allowLinks) {
15 allowedTags.push("a");
16 allowedAttributes.a = ["href", "title", "target", "rel"];
17 }
18 
19 if (opts.allowImages) {
20 allowedTags.push("img");
21 allowedAttributes.img = ["src", "alt", "width", "height"];
22 }
23 
24 return sanitizeHtml(dirty, {
25 allowedTags,
26 allowedAttributes,
27 allowedSchemes: ["http", "https", "mailto"],
28 allowedSchemesByTag: { img: ["http", "https", "data"] },
29 disallowedTagsMode: "discard",
30 transformTags: {
31 a: (tagName, attribs) => ({
32 tagName,
33 attribs: {
34 ...attribs,
35 target: "_blank",
36 rel: "noopener noreferrer nofollow",
37 },
38 }),
39 },
40 exclusiveFilter: (frame) =>
41 frame.tag === "a" && !frame.text.trim() && !frame.attribs.href,
42 });
43}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1An allowlist approach is safer than blocklisting because anything unlisted is dropped by default.
  2. 2Optional feature flags let you widen the permitted markup only when the caller explicitly needs it.
  3. 3Rewriting attributes on the way out lets you enforce security defaults like noopener on every link.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a configurable HTML sanitizer allowlist — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code