LEARNING OBJECTIVES ⌵
- Understand why "one-size-fits-all" encoding fails and how browser parsers transition across execution contexts.
- Implement precise contextual encoding algorithms for HTML Body, HTML Attributes, JavaScript literals, URIs, and CSS.
- Prevent
</script>tag breakout attacks in server-side JSON state hydration pipelines. - Build a robust multi-context sanitization and encoding utility.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a diplomatic passport carrying a message across five international borders: France, Germany, China, Russia, and Japan.
If you translate an English message into German, it will be understood in Berlin. But if you hand that same German document to a border guard in Tokyo who only speaks Japanese, the meaning is corrupted or misinterpreted. Worse, if a malicious phrase sounds harmless in French but acts as a treasonous command in Russian, you trigger a catastrophe at the Russian border.
In web browsers, a single document contains multiple sub-languages with totally different syntax rules: HTML, CSS, JavaScript, and URIs.
Converting < into < protects an HTML body tag (<div><script></div>). However, if you inject that exact same string into a JavaScript variable inside a <script> tag (const user = '<script>';), the JavaScript engine does not decode HTML entities; it treats < as literal characters, but if the attacker enters "; alert(1); //, the HTML encoder leaves quotes intact and the script executes!
Encoding must be strictly contextual: the transformation algorithm must match the exact parser that will interpret that specific slice of text.
Technical Deep Dive & Specifications
The 5 Web Parser Contexts
When a browser renders a web page, its tokenizer constantly switches states between distinct parsers:
+-------------------------------------------------------------------------------+
| THE 5 BROWSER ENCODING CONTEXTS |
+-------------------------------------------------------------------------------+
1. HTML BODY CONTEXT:
<div>[USER DATA HERE]</div>
-> Rule: Encode HTML special characters (&, <, >, ", ') to named/numeric entities.
2. HTML ATTRIBUTE CONTEXT:
<input type="text" name="fname" value="[USER DATA HERE]">
-> Rule: Attribute values MUST be quoted; encode quotes, ampersands, and angle brackets.
3. JAVASCRIPT VARIABLE / JSON CONTEXT:
<script>const state = "[USER DATA HERE]";</script>
-> Rule: Unicode/Hex escape quotes, backslashes, and explicitly escape '</script>'.
4. URI / URL CONTEXT:
<a href="/search?q=[USER DATA HERE]">Search</a>
-> Rule: Percent-encode (RFC 3986) via encodeURIComponent; validate scheme!
5. CSS PROPERTY CONTEXT:
<div style="color: [USER DATA HERE];">Text</div>
-> Rule: Strict alphanumeric allowlisting or CSS hex escaping (\3C ).
Contextual Encoding Specifications Matrix
| Context | Example HTML Location | Attack Syntax Breakout | Required Encoding / Strategy | Safe Output Example |
|---|---|---|---|---|
| 1. HTML Body | <div>DATA</div> |
<script>, <img> |
& → &< → <> → >" → "' → ' |
<b>Hello</b> |
| 2. Quoted Attribute | <input value="DATA"> |
" onfocus="alert(1) |
All HTML body entities plus ASCII hex for quotes | " onfocus="... |
| 3. Unquoted Attribute | <input value=DATA> |
[space] onfocus=... |
NEVER USE UNQUOTED ATTRIBUTES (Space, Tab, Newline, >, = break attributes) |
Always quote attributes! |
| 4. JavaScript String | <script>let x = 'DATA';</script> |
'; alert(1); // or </script> |
Unicode-escape characters: \ → \\" → \u0022' → \u0027< → \u003C |
\u0027; alert(1); |
| 5. URI Parameter | <a href="/profile?id=DATA"> |
&id=2 or javascript:... |
encodeURIComponent(DATA) |
john%20doe%26admin%3Dtrue |
The </script> Tag Breakout Vulnerability in JSON Hydration
A pervasive vulnerability in Single Page App (SPA) server-side rendering (Next.js, Nuxt, Remix) occurs when embedding server state directly into HTML:
<!-- ❌ INSECURE SSR HYDRATION -->
<script>
window.__INITIAL_STATE__ = <%= JSON.stringify(untrustedData) %>;
</script>
Why JSON.stringify() is NOT enough:
The HTML parser has higher precedence than the JavaScript engine. When the HTML parser reads inside a <script> tag, it scans strictly for the closing sequence </script> (case-insensitive).
If untrustedData contains: "</script><script>alert('XSS')</script>", JSON.stringify() produces:
<script>
window.__INITIAL_STATE__ = "<\/script><script>alert('XSS')<\/script>";
</script>
The HTML tokenizer encounters </script>, immediately terminates the script block, and interprets <script>alert('XSS')</script> as a brand-new live executable script tag!
The Fix: Serialization Escaping
Replace < and / characters with their Unicode equivalents:
function safeJsonStringify(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003C')
.replace(/>/g, '\\u003E')
.replace(/\//g, '\\u002F')
.replace(/\u2028/g, '\\u2028') // Line separator (JS syntax error)
.replace(/\u2029/g, '\\u2029'); // Paragraph separator (JS syntax error)
}
💻 Interactive Code Playground
Starter Code
The following application provides a live multi-context encoder tool, demonstrating how the same payload must be encoded differently depending on where it will be placed.
Line-by-Line Code Breakdown
- Line 33–42:
htmlBodymethod replaces the 5 critical HTML syntax characters with named XML entities (&,<,>,",'). - Line 52–65:
jsStringconverts quotation marks, backslashes, and angle brackets into 4-digit hexadecimal Unicode escape sequences (\u003C,\u0022), preventing string termination and tag breakouts. - Line 67–69:
uriComponentdelegates to nativeencodeURIComponent(), translating spaces to%20and symbols to percent-encoded octets. - Line 76: When outputting JS context, note how
</script>is safely escaped to<\/script>in string literals.
Expected Browser Render Output
For the input payload <script>alert("XSS & 'pwned'");</script>:
- HTML Body:
<script>alert("XSS & 'pwned'");</script> - HTML Attribute:
<input type="text" value="<script>alert("XSS & 'pwned'");</script>"> - JavaScript Context:
<script> const userPayload = "\u003Cscript\u003Ealert(\u0022XSS & \u0027pwned\u0027\u0022);\u003C\u002Fscript\u003E"; </script> - URI Component:
<a href="https://example.com/search?q=%3Cscript%3Ealert(%22XSS%20%26%20'pwned'%22)%3B%3C%2Fscript%3E">Link</a>
🏋️ Hands-On Exercise
🎯 The Challenge: Fix a Broken Multi-Context Profile Card
Instructions:
- You are given a script that renders a user profile card containing:
- A username displayed in an HTML
<h3>tag. - An email placed inside an
<input value="...">attribute. - A user preference JSON object placed inside an inline
<script>tag. - A personalized homepage URL placed inside an
<a href="...">link.
- A username displayed in an HTML
- The current code applies HTML entity encoding everywhere, which breaks the JavaScript and URI contexts while leaving the SSR script block vulnerable to breakout.
- Fix the rendering function so that each field uses its appropriate contextual encoder and URL scheme validator.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- HTML Entity Encoding Inside JavaScript Code: Writing
var data = "<script>";. In JavaScript,<is not decoded to<; it remains literal text. However, if the data contains unescaped quotes ("), the HTML encoder ignores it and your JS string breaks. - Double-Encoding Errors: Running an already-encoded string through an encoder a second time (e.g.,
&becomes&amp;). Track whether data is in raw or encoded state through strict typing. - Unquoted HTML Attributes: Writing
<div class=${userInput}>. IfuserInputcontains spaces (e.g.,foo onmouseover=alert(1)), the browser interprets the space as the delimiter between attributes, creating a live event handler.
💡 Pro Tips
- Use
serialize-javascriptfor SSR Hydration: In Node.js / Next.js production backends, use the battle-testedserialize-javascriptlibrary from Yahoo to serialize server state safely. - Automate Contextual Escaping with Template Engines: Modern template engines like Mustache, Handlebars, and JSX automatically apply HTML context escaping; ensure developers do not bypass them with triple braces
{{{ raw }}}orv-html.
📌 Key Takeaways
- There is no single universal encoding algorithm; transformations must be context-aware.
- The 5 core web contexts are HTML Body, HTML Attribute, JavaScript Literal, URI Parameter, and CSS Property.
JSON.stringify()alone is insufficient for embedding data into inline<script>tags because the HTML tokenizer looks for</script>before JS is parsed.- Always quote HTML attribute values (
value="..."); unquoted attributes allow attribute injection via simple spaces. - Validate URI schemes (
http:,https:) before placing user inputs intohreforsrcattributes. - --