HTML Sanitisation
Sanitisation removes dangerous HTML/JS from user input that you intend to render as HTML (rich text editors, markdown, oEmbed). Different from encoding: sanitisation modifies the input; encoding makes it inert at output.
DOMPurify, sanitize-html, server-side
EXAMPLE
// 1) DOMPurify (browser + JSDOM on server) — the de-facto standard
import DOMPurify from 'dompurify';
const dirty = `<img src=x onerror=alert(1)><p>hello</p>`;
const clean = DOMPurify.sanitize(dirty);
// → "<img src=\"x\"><p>hello</p>" — onerror is gone
element.innerHTML = clean;
// 2) Stricter allowlist
const safe = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'code', 'pre', 'ul', 'ol', 'li', 'h2', 'h3', 'blockquote'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false,
});
// 3) Make every link safe — open in new tab + lose referrer
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node.hasAttribute('href')) {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
});
// 4) Block dangerous URI schemes — javascript:, data:, vbscript:
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
if (['href', 'src', 'action', 'formaction'].includes(data.attrName)) {
if (/^\s*(javascript|data|vbscript):/i.test(data.attrValue)) {
data.keepAttr = false;
}
}
});
// 5) Server-side — sanitize-html for Node (no JSDOM dependency)
// npm i sanitize-html
import sanitizeHtml from 'sanitize-html';
const out = sanitizeHtml(input, {
allowedTags: ['p', 'br', 'strong', 'em', 'a', 'code', 'pre', 'ul', 'ol', 'li', 'h2', 'h3'],
allowedAttributes: { a: ['href', 'title', 'target', 'rel'] },
allowedSchemes: ['http', 'https', 'mailto'],
transformTags: {
a: sanitizeHtml.simpleTransform('a', { rel: 'noopener noreferrer', target: '_blank' }),
},
disallowedTagsMode: 'discard',
});
// 6) Python — bleach
// pip install bleach
import bleach
clean = bleach.clean(
html,
tags=['p', 'br', 'strong', 'em', 'a', 'code', 'pre', 'ul', 'ol', 'li'],
attributes={'a': ['href', 'title']},
protocols=['http', 'https', 'mailto'],
strip=True,
)
// 7) PHP — HTMLPurifier
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,br,strong,em,a[href|title],code,pre,ul,ol,li');
$config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true]);
$purifier = new HTMLPurifier($config);
$clean = $purifier->purify($dirty);
// 8) Markdown → HTML — sanitise the OUTPUT, not the input
// 1. Render with markdown-it / marked
// 2. Run the resulting HTML through DOMPurify
// (markdown parsers themselves can produce unsafe HTML if you allow raw HTML blocks)
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt({ html: false, linkify: true });
const rendered = md.render(markdownInput);
const safeHtml = DOMPurify.sanitize(rendered);
// 9) When to sanitise
// - On STORE: lets you also serve cached HTML cheaply; but you can't tighten the policy retroactively.
// - On RENDER: the policy stays editable; slight per-request CPU cost.
// Most teams sanitise on render — costs less, more flexible.
// 10) When NOT to use sanitisation
// If you can avoid raw HTML entirely — DO. Render markdown, BBCode, or your own DSL.
// Sanitisation is the right tool only when users genuinely need to paste rich HTML (CMS, email composers).
// 11) Anti-patterns
// • Regex to 'remove <script> tags' — countless bypasses (<scr<script>ipt>, attribute payloads)
// • Allowing inline styles — `style=expression(...)` in legacy IE, CSS injection elsewhere
// • Allowing <svg> without restricting its sub-tags — <use>, animation, foreignObject can carry payloads
// • Trusting the front-end sanitiser only — duplicate on the server (and your tests prove both)
// • Showing the sanitised output in an <iframe> with allow-same-origin — defeats sandboxing
// 12) Test corpus — keep a known-bad string list
// e.g. OWASP XSS Filter Evasion Cheat Sheet
// Run sanitiser over them in CI; assert all are neutralised.
// 13) Combine with CSP
// Even a misconfigured sanitiser can let something through. CSP (script-src + nonce) limits the damage.
// Belt + braces.
Why it matters
Sanitise on render with DOMPurify (client) or sanitize-html / bleach / HTMLPurifier (server). Pair with a strict CSP and regex-only solutions go in the bin — they never cover all the bypasses.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Prefer escaping. If you must allow rich HTML (comments, CMS), sanitise:
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty, {
USE_PROFILES: { html: true },
FORBID_ATTR: ['style', 'onerror'],
});
Try it Yourself »
Exercise
Sanitise rich HTML before assigning.
el.innerHTML =
.sanitize(rich);
PascalCase.
Discussion
Loading…