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

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.

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