import { JSDOM } from 'jsdom';
import createDOMPurify, { DOMPurifyI } from 'dompurify';
const { window } = new JSDOM('');
const DOMPurify: DOMPurifyI = createDOMPurify(window as unknown as Window);
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node.hasAttribute('target')) {
node.setAttribute('rel', 'noopener noreferrer');
}
});
const CONFIG = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'title', 'target'],
ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|tel:|#|\/)/i,
KEEP_CONTENT: true,
};
export function createSanitizer() {
return function sanitize(dirty: string): string {
return DOMPurify.sanitize(dirty, CONFIG).trim();
};
}
export const sanitizeHtml = createSanitizer();
import { Router, Request, Response } from 'express';
import { sanitizeHtml } from './sanitizer';
import { Comment } from './models/Comment';
export const commentsRouter = Router();
commentsRouter.post('/comments', async (req: Request, res: Response) => {
const { postId, body } = req.body as { postId?: string; body?: string };
if (!postId || typeof body !== 'string') {
return res.status(400).json({ error: 'postId and body are required' });
}
const clean = sanitizeHtml(body);
if (clean.length === 0) {
return res.status(422).json({ error: 'Comment has no renderable content' });
}
const comment = await Comment.create({
postId,
authorId: req.user.id,
bodyHtml: clean,
});
return res.status(201).json({ id: comment.id, bodyHtml: comment.bodyHtml });
});
This snippet shows how untrusted HTML from user input is sanitized on the server before being stored or rendered, using DOMPurify bound to a JSDOM window. DOMPurify is a browser library that needs a live DOM to walk and mutate the parsed markup, so on Node it must be wired to a synthetic window — that is exactly what sanitizer.ts does.
In sanitizer.ts, a single JSDOM instance is created once at module load and passed to createDOMPurify, which is far cheaper than spinning up a new window per request. The createSanitizer factory returns a sanitize function preconfigured with an allow-list: ALLOWED_TAGS and ALLOWED_ATTR restrict output to a small set of formatting and link elements, and ALLOWED_URI_REGEXP blocks javascript: and data: URIs that are a classic vector for script injection through href. The key idea of sanitization is that a deny-list is unwinnable — attackers find encodings and edge cases faster than they can be blacklisted — so a strict allow-list of known-safe tags and attributes is used instead.
A hardening hook is registered via addHook('afterSanitizeAttributes'): any anchor that ends up with a target attribute also gets rel="noopener noreferrer", closing the reverse-tabnabbing hole where a linked page can manipulate window.opener. Because RETURN_DOM and friends are left off, sanitize returns a clean HTML string ready to persist.
commentsRouter.ts demonstrates the boundary where this matters. The POST /comments handler treats the raw body as hostile, runs it through sanitizeHtml before anything else, and rejects the request with 422 if sanitization strips the content down to nothing — a cheap way to catch payloads that were pure markup or script. Only the sanitized string is passed to Comment.create, so the database never holds dangerous markup.
The important trade-off is when to sanitize. Sanitizing on input (as shown) keeps stored data clean and makes reads fast, but couples stored content to the current allow-list; sanitizing on output preserves the original but pays the cost on every render. A common pitfall is trusting client-side sanitization alone — it is trivially bypassed by hitting the API directly, so the server pass in commentsRouter.ts is the real security control. Escaping at render time remains a valuable second layer for defense in depth.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Semantic HTML Example</title>
Semantic HTML5 elements and accessibility best practices
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
Share this code
Here's the card — post it anywhere.