Gguestjust now
Escape output, never innerHTML
Let the framework escape; sanitize HTML with an allow-list.
From security-best-practices: no eval(), no innerHTML.
Same string, two sinks
Type or pick a payload.
el.innerHTML = inputBad0
DOM it would build
el.textContent = inputGood01 text node
comment.textContent = userInput;
<p>{userInput}</p> // React
<p>{{ userInput }}</p> // Vue
Parse, then allow-list
Pick a payload, then change the allow-list.
Regex blacklistBad
0
Output
.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/javascript:/gi, "")Parse + allow-listGood
0
Output
Renders as
import DOMPurify from 'isomorphic-dompurify';
const sanitized = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['p', 'b', 'strong', 'i', 'em', 'a', 'ul', 'li', 'br'],
ALLOWED_ATTR: ['href'],
});
// only ever sanitized output
<div dangerouslySetInnerHTML={{ __html: sanitized }} />
A calculator that runs code
Type or pick an expression.
eval(input)BadWould execute
eval()calc(input)GoodParsed
0-9 . + - * / % ( )function calc(src) {
let i = 0;
const skip = () => { while (/\s/.test(src[i] ?? "")) i++; };
const fail = () => { throw new SyntaxError(`Unexpected "${src[i] ?? "end"}" at ${i}`); };
const factor = () => {
skip();
if (src[i] === "-") { i++; return -factor(); }
if (src[i] === "(") { i++; const v = sum(); skip(); if (src[i] !== ")") fail(); i++; return v; }
const m = /^\d*\.?\d+/.exec(src.slice(i));
if (!m) fail();
i += m[0].length;
return Number(m[0]);
};
const product = () => { let v = factor(); for (skip(); "*/%".includes(src[i] || "_"); skip()) { const op = src[i++]; const r = factor(); v = op === "*" ? v * r : op === "/" ? v / r : v % r; } return v; };
const sum = () => { let v = product(); for (skip(); "+-".includes(src[i] || "_"); skip()) { const op = src[i++]; const r = product(); v = op === "+" ? v + r : v - r; } return v; };
const v = sum(); skip();
if (i < src.length) fail();
return v;
}
Why it matters, pros and cons
Why it matters
Once markup and user text share a string, the browser can't tell them apart.
Pros
- Framework escaping is free and default.
- Kills the bug class, not one payload.
- One rule a linter can grep for.
Cons
textContentdoes not make anhrefsafe.- A sanitizer adds bundle weight and parse time.
- Editing sanitized output can rebuild a payload.