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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1An allowlist approach is safer than blocklisting because anything unlisted is dropped by default.
- 2Optional feature flags let you widen the permitted markup only when the caller explicitly needs it.
- 3Rewriting attributes on the way out lets you enforce security defaults like noopener on every link.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
typescript
import { Injectable, Scope, Inject, NotFoundException } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; import { DataSource } from 'typeorm';
Per-tenant database connections in NestJS
multi-tenancy
connection-pooling
dependency-injection
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-configurable-html-sanitizer-allowlist-explained-typescript-d65e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.