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

Output Encoding

Output encoding turns dangerous characters into safe ones at the moment of rendering. Different contexts (HTML body, attribute, JS, URL, CSS) need different encodings — one-size-fits-all gets you XSS.

Per-context encoding, library defaults

EXAMPLE
// 1) HTML body — escape & < > " '
function encodeHtml(s) {
    return String(s)
        .replaceAll('&',  '&amp;')
        .replaceAll('<',  '&lt;')
        .replaceAll('>',  '&gt;')
        .replaceAll('"', '&quot;')
        .replaceAll("'", '&#39;');
}

// In a template:
// <p>${encodeHtml(userInput)}</p>

// 2) HTML attribute — quoted attributes still need &amp; / &quot;
// <input value="${encodeHtml(userInput)}">
// Unquoted attributes are dangerous — never emit one with user data

// 3) JavaScript string — escape backslash, quote, control chars
function encodeJsString(s) {
    return String(s).replace(/[\\\"'<>\u0000-\u001f\u2028\u2029]/g, c => {
        const code = c.charCodeAt(0).toString(16).padStart(4, '0');
        return `\\u${code}`;
    });
}
// <script>const name = "${encodeJsString(userName)}";</script>
// Better: emit JSON.stringify(value) — it handles all of this
//   <script>const data = ${JSON.stringify(payload)};</script>

// 4) URL components — encodeURIComponent for query-string values
const safe = `?q=${encodeURIComponent(userQuery)}`;
// Don't use encodeURI for parts; it's too lenient.

// 5) CSS — least-safe context, avoid putting user data in CSS at all
// If you must:
function encodeCss(s) {
    return s.replace(/[^\w-]/g, c => `\\${c.charCodeAt(0).toString(16)} `);
}

// 6) Framework defaults — most frameworks encode HTML by default

// React
<div>{userInput}</div>          // SAFE — auto-escaped
<div dangerouslySetInnerHTML={{ __html: dirtyHtml }} />  // ONLY if you sanitised with DOMPurify

// Vue
<p>{{ userInput }}</p>          // SAFE — interpolation escapes
<p v-html="userInput"></p>      // DANGEROUS unless sanitised

// Svelte
<p>{userInput}</p>              // SAFE
{@html userInput}              // unsafe — sanitise first

// Angular
<p>{{ userInput }}</p>          // SAFE
<div [innerHTML]="userInput"></div>  // Angular's DomSanitizer strips known-bad by default

// Blade (Laravel)
{{ $userInput }}                 // SAFE — htmlspecialchars()
{!! $userInput !!}               // DANGEROUS — raw output

// 7) JSON in templates — use the framework helper
// Rails:        <script>const d = <%= raw payload.to_json %></script>          // bad
// Rails better: <script>const d = <%= json_escape(payload.to_json) %></script>
// Laravel:      <script>const d = @json($payload)</script>
// Django:       <script>const d = {{ payload|json_script:"d" }}</script>  + parse from <script id="d">

// 8) Common mistakes
//   • One encoding for all contexts (e.g. just `&lt;`) — fails in JS / URL / CSS
//   • Double-encoding (data already encoded gets encoded again — &amp;amp;lt;)
//   • Stripping characters instead of encoding — UX worse, still buggy
//   • Encoding on STORE — once you decide on the storage form, encoding still happens
//     at output time per context

// 9) Test for XSS — combined with output encoding
// Common payloads:
//   <script>alert(1)</script>
//   <img src=x onerror=alert(1)>
//   "; alert(1); //
//   javascript:alert(1)
// If your output encoder is right, none of these should execute when reflected.

// 10) Defense in depth — encoding + CSP
// Even with perfect encoding, add a strict CSP (nonce, no unsafe-inline) so a missed
// spot can't execute injected JS. See the csp lesson for the recipe.

// 11) Tools
// - DOMPurify        — when you must accept rich HTML
// - js-string-escape — robust JS string encoder
// - he               — html entity encoder/decoder

Why it matters

Encoding is per-context. The same value in <p>, onclick=, href=, and <script> needs four different escapes — that’s why frameworks bake context awareness into their templating engines.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// HTML context
encodeURIComponent(...)         // for URL params
template `${escapeHtml(v)}`     // for HTML text
// Modern frameworks (React/Vue/Svelte/Blade/Razor) escape by default;
// the bug is when you reach for an "unsafe" / "raw" API.
Try it Yourself »

Discussion

Loading…