Context-Aware Encoding
Output context decides what counts as “safe” output. HTML body, attribute, JavaScript, URL, CSS, JSON — each needs different encoding. Wrong context = XSS even with “encoded” output.
Per-context encoding rules
EXAMPLE
// 1) HTML body context — encode &, <, >, ", '
function encHtml(s) {
return String(s)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
// Template:
// <p>${encHtml(userInput)}</p>
// Output: <p><script>alert(1)</script></p> ← safe
// 2) HTML attribute context — quoted attribute, additional rules
// <input value="${encHtml(userInput)}"> ← OK (encHtml covers quoted attrs)
// <input value=${userInput}> ← UNSAFE — attacker injects ` onclick=alert(1)`
// 3) JavaScript string context — completely different rules
// <script>const name = "${???}";</script>
// HTML encoding doesn't help here. Need JS string escaping or, better, JSON.stringify:
function encJsString(s) {
return String(s).replace(/[\\"'<>\u0000-\u001f\u2028\u2029]/g, c => {
return '\\\\u' + c.charCodeAt(0).toString(16).padStart(4, '0');
});
}
// <script>const name = "${encJsString(userName)}";</script>
// Better: emit JSON and use it as data
// <script>const data = ${JSON.stringify(payload)};</script>
// Even better: don't inject — read from a data attribute
// <div id="app" data-user="${encHtml(JSON.stringify(payload))}"></div>
// <script>const data = JSON.parse(document.getElementById('app').dataset.user);</script>
// 4) JavaScript identifier / property name context
// <script>data.${???} = 1;</script>
// Allowlist: only legal JS identifiers
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) throw new Error('invalid key');
// 5) URL context
// <a href="/profile?id=${???}">
// Use encodeURIComponent for query VALUES
const link = `/profile?id=${encodeURIComponent(userId)}`;
// Whole URL:
const link2 = `/posts/${encodeURIComponent(slug)}`;
// AVOID javascript: protocol — always check schemes
function safeHref(url) {
try {
const u = new URL(url, location.origin);
if (!['http:', 'https:', 'mailto:'].includes(u.protocol)) return '#';
return u.toString();
} catch { return '#'; }
}
// <a href="${encHtml(safeHref(userUrl))}">
// 6) CSS context — least-safe; AVOID user input here
// <div style="color: ${???}">
// Allowlist a color set or convert through a paint API; never paste raw CSS.
const COLORS = { red: '#ef4444', blue: '#0ea5e9', green: '#10b981' };
const color = COLORS[req.body.color] ?? '#000';
// 7) JSON context (script tag with JSON data)
// <script type="application/json" id="app-data">${encJsonInHtml(JSON.stringify(payload))}</script>
// Replace </ to prevent </script> closing the tag from data:
function encJsonInHtml(json) {
return json.replace(/</g, '\\\\u003c');
}
// 8) HTML comment context — also dangerous
// <!-- ${???} --> → attacker injects --><script>alert(1)</script><!--
// Strip / encode any -- and --> sequences in user data; better: don't put user data in comments.
// 9) XML / SVG context
// Same risks as HTML; use a DOMParser / serialiser; sanitise with DOMPurify.
// === Per-framework defaults ===
// React: text content auto-escaped; dangerouslySetInnerHTML bypasses safety
<div>{userInput}</div> // SAFE — HTML-escaped
<div dangerouslySetInnerHTML={{ __html: dirty }} /> // ONLY after DOMPurify
// Vue
<p>{{ userInput }}</p> // SAFE
<p v-html="userInput"></p> // UNSAFE without sanitise
// Svelte
<p>{userInput}</p> // SAFE
{@html userInput} // UNSAFE
// Angular
<p>{{ userInput }}</p> // SAFE
<div [innerHTML]="userInput"></div> // Angular's DomSanitizer strips known-bad
// Blade (Laravel)
{{ $userInput }} // SAFE
{!! $userInput !!} // UNSAFE
// === Multi-context output ===
// 10) The hardest case: user input that lands in TWO contexts
// <input type="text" value="${encHtml(name)}" onclick="alert('hi, ${???}');">
// The onclick value is HTML-attribute + JS-string. You need BOTH encodings:
// 1. JS-escape for the JS string
// 2. HTML-escape for the attribute
// const escaped = encHtml(encJsString(name));
// Better: bind via JS, not inline:
// <input type="text" value="${encHtml(name)}" data-name="${encHtml(name)}">
// <script>document.querySelectorAll('input').forEach(i => i.onclick = () => alert(`hi, ${i.dataset.name}`));</script>
// === Practical guidance ===
// 11) Default to framework escaping
// React / Vue / Svelte / Angular / Blade all escape HTML content by default.
// Only bypass when you intentionally need to render trusted HTML (sanitise first).
// 12) Common bugs
// • Same encoder for all contexts → JS injection slips through HTML-only encoding
// • Encoding the input before storing, then encoding again on output (double-encoding)
// • Forgetting that attributes without quotes need different rules
// • Trusting javascript:/data: URLs from user input
// • Inline event handlers (onclick=) with user data
// 13) Safer architectures
// • Use data-* attributes + JS reads → never inject into <script> bodies
// • Use JSON.stringify for any data passed from server to JS
// • Use the URL() constructor to validate href values
// • Use a strict CSP (nonce + strict-dynamic) — catches what encoding misses
// 14) Test corpus
// Maintain a list of XSS payloads (OWASP XSS Filter Evasion Cheat Sheet)
// Run them through every input → render path; assert all are neutralised.
// 15) Quick context decision
// HTML body → htmlEscape
// Quoted HTML attribute → htmlEscape
// Unquoted HTML attribute → AVOID (force quotes around user data)
// <script> string content → JSON.stringify (always JSON, not handwritten JS)
// URL component → encodeURIComponent
// javascript: scheme → reject
// CSS → allowlist; do NOT pass raw user input
// HTML comment → don't put user data there
Why it matters
There’s no “just escape it” — HTML context, JS string, URL, CSS each need different escaping. Use framework defaults; reach for context-specific encoders only when you must hand-roll output.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Escaping is CONTEXT-specific. The right encoding depends on where data lands: // HTML body → HTML-entity encode // HTML attribute → quote + HTML-entity encode // JavaScript string → JS string escape // URL component → encodeURIComponent // CSS → CSS escape // Use a library that picks the right one (e.g. OWASP Java Encoder).Try it Yourself »
Discussion
Loading…