iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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('&',  '&amp;')
        .replaceAll('<',  '&lt;')
        .replaceAll('>',  '&gt;')
        .replaceAll('"', '&quot;')
        .replaceAll("'", '&#39;');
}

// Template:
// <p>${encHtml(userInput)}</p>
// Output: <p>&lt;script&gt;alert(1)&lt;/script&gt;</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…