XSS Prevention Review: Contextual Output Encoding
XSS prevention depends on output context. HTML text, quoted attribute, URL, JavaScript string, and CSS each need different encoding; one escaping function cannot safely cover all.
Review sinks, not labels
Trace untrusted value to DOM or server template sink. Example: a display name is safe in HTML text after HTML encoding but unsafe when inserted into onclick. Example: a return URL needs scheme validation before URL encoding.
Context-to-encoding table
| Sink | Control | Negative test |
|---|---|---|
| HTML text | HTML encoder | tag renders as text |
| attribute | attribute encoder plus quoting | quote cannot escape |
| URL | allowlisted scheme | javascript: refused |
| rich text | maintained sanitizer | event handler removed |
OWASP XSS prevention and CWE-79 explain these distinctions.
Decision rule: never pass untrusted string to an unsafe DOM sink without context-specific control.
Remediation
Prefer framework auto-escaping, remove dangerous sinks, sanitize rich text, and add regression payloads per context. CSP helps contain impact but cannot correct wrong encoding. Code review can follow template and client paths.
Review rendering, not only input
Inventory every template interpolation, client-side sink, rich-text boundary, URL builder, and framework escape hatch. Let templates encode ordinary HTML text. Use quoted fixed attributes and avoid putting untrusted values in event handlers, raw scripts, selectors, or markup construction. For links, validate allowed scheme and host policy before encoding a component and then encoding attribute context. Use textContent rather than HTML parsing where text is intended.
Test one payload per named context and preserve rendered output, not only source code. Include quote escape, tag text, javascript URL, inline event, JSON breaker, and sanitizer regression. Owner: frontend owner. Pass: content renders as data and no script executes. Fail: untrusted value changes DOM structure, URL scheme, or script behavior. Exceptions must identify sink, temporary restriction, compensating CSP or isolation, risk owner, remediation task, and expiry.
A sink-by-sink artifact
Keep a table with route or component, value source, exact sink, context, required transform, and negative payload. A customer name in HTML text uses template escaping; an outbound destination receives scheme validation, component encoding, then quoted attribute encoding. This map exposes raw rendering escape hatches introduced by later framework changes.
Rendering-context trace
Expected output: source field, component, sink API, context class, encoder or sanitizer version, payload ID, rendered DOM result. This context trace proves whether untrusted characters remained inert after rendering.
Worked failure: A display name containing markup reaches a text node and must render characters, not an element. A URL field must fail scheme validation before attribute encoding.
Edge case: Rich-text sanitization is not safe after downstream concatenation. Keep sanitized output immutable or re-sanitize after any intentional transformation.
Closure test: Execute browser regression payloads against each mapped sink. Closure passes when DOM contains data only and no event handler or script executes.
Context inventory exercise
Trace values from request parameters, database fields, imported documents, URL fragments, and browser storage to their final rendering sites. Classify each use as text node, quoted attribute, URL component, JSON data, style value, or approved rich HTML. Any value without a known context is a review finding, not a candidate for generic escaping. Framework helpers should be tested at their escape hatches, including raw HTML properties and client template functions.
A worked browser fixture can set a display name to markup, an image URL to a disallowed scheme, and a comment to sanitizer-bypass candidates. Expected output is visible encoded text, rejected URL, and removed unsafe markup. Review source maps or bundled code for sinks introduced after compilation. If a product requires HTML, define allowed elements and attributes as a content policy; do not describe a sanitizer library name as sufficient evidence.
A closure fixture should exercise server and client rendering of same value. Expected output differs by context but remains inert everywhere. Review must fail if code converts encoded text back into markup before browser receives it.
DOM review must include values assembled after initial page load. Search event listeners, client router parameters, postMessage handlers, and storage reads for untrusted data reaching HTML-capable APIs. A server-rendered page can be safe while a later client update introduces DOM XSS. Use a test that changes route state after load, then inspect resulting DOM. Document whether Trusted Types is available as additional control, but do not treat unsupported browsers as covered by it.
Final review maps every rendering sink to its output context, encoder, and hostile fixture result. Expected result: displayed data remains data without script execution or unintended DOM mutation. Edge case: third-party template combines URL and attribute contexts; test rendered result. Close only after deployed server rendering and client updates both remain inert.