Tagged Template Sanitization Idea
Use tagged templates to auto-escape interpolated values for HTML — why it beats string concat, and limits vs real sanitizers.
- javascript
- tagged-template
XSS often starts as HTML built with string concatenation and untrusted input. Tagged templates receive raw string chunks and interpolated values separately, so you can escape values by default and only allow trusted HTML intentionally.
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function html(strings, ...values) {
return strings.reduce((out, str, i) => {
const v = i < values.length ? escapeHtml(values[i]) : '';
return out + str + v;
}, '');
}
const user = '<img src=x onerror=alert(1)>';
const out = html`<p>Hello ${user}</p>`;
// <p>Hello <img src=x onerror=alert(1)></p>
Why the split matters
// Dangerous
el.innerHTML = `<p>Hello ${user}</p>`;
// Tag sees values separately — can force escape
el.innerHTML = html`<p>Hello ${user}</p>`;
The tag function is just a function: first arg is TemplateStringsArray, rest are values.
html(['a', 'b'], user); // same mechanism without sugar — sugar is the point for DX
Trusted raw HTML
Sometimes you need a trusted fragment (Markdown render output you already sanitized). Use a branded type:
class SafeHtml {
constructor(s) { this.s = s; }
toString() { return this.s; }
}
function html(strings, ...values) {
return strings.reduce((out, str, i) => {
let v = '';
if (i < values.length) {
const raw = values[i];
v = raw instanceof SafeHtml ? raw.s : escapeHtml(raw);
}
return out + str + v;
}, '');
}
const body = new SafeHtml(sanitizedFromDOMPurify);
html`<article>${body}</article>`;
Without a SafeHtml escape hatch, every value is escaped — good default.
Limits (don’t stop at escapeHtml)
| Attack surface | Simple entity escape |
|---|---|
| Text in HTML body | mostly OK |
| Attribute contexts | need quote-aware rules |
javascript: URLs |
not fixed by <> escape alone |
| CSS / JS contexts | different escaping |
| SVG / MathML | extra rules |
For rich HTML from users, run DOMPurify (or server sanitizer) and then mark SafeHtml. Tagged templates are a composition UX, not a full HTML security program.
Related patterns
Libraries (lit-html, Emotion’s older APIs, GraphQL gql) use tagged templates for DSLs. Same idea: parse strings + values into a safer structure than one big string.
// lit-style mental model: return a TemplateResult, not immediately dump to DOM
Interview answer (out loud)
“A tagged template gets string chunks and interpolations separately so I can HTML-escape values by default. That’s safer than concatenating untrusted strings into innerHTML. I still use a real sanitizer for rich HTML and special-case attributes/URLs. SafeHtml branding marks pre-sanitized content.”
Attribute-aware sketch (still incomplete)
function attr(s) {
return escapeHtml(s).replace(/`/g, '`');
}
// Real systems use context-specific encoders (HTML body vs attr vs URL)
Frameworks that claim XSS safety (React’s default text children, lit’s html tag) implement context tracking. Replicating that in 20 lines is educational — shipping it alone is not.
CSP defense in depth
Even with escaping, Content-Security-Policy without unsafe-inline limits damage if something slips through. Tags + CSP + sanitizer for rich HTML is the layered approach.
Further reading
Related
- Template Literals and Tagged Templates
- Creating and Updating DOM Nodes
- Cookies for Frontend Engineers
- postMessage and Origin Checks
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.