JSON.stringify() and its danger
The problem: injecting unescaped JSON into HTML
During SSR you often need to pass data from the server to the client, so React (or another framework) can "hydrate" the page without loading it again.
A typical example
// On the server:
const data = { user: { name: 'Tim' } };
const html = `
<html>
<body>
<div id="root">${reactHtml}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(data)};
</script>
</body>
</html>
`;
res.send(html);At first glance, this looks fine.
But if data contains user-supplied data, the problem will not keep you waiting
What can go wrong
If data contains special HTML or JS characters,
JSON.stringify() inserts them as is into the HTML, without escaping.
An example of user input:
const data = {
user: { name: '</script><script>alert("XSS!")</script>' }
};After JSON.stringify(data) we get:
<script>
window.__INITIAL_DATA__ = {"user":{"name":"</script><script>alert(\"XSS!\")</script>"}};
</script>What happens:
- the browser closes the first
<script>tag (</script>) - and runs the malicious script
alert("XSS!").
Congratulations, a classic XSS through SSR.
Why this happens
JSON.stringify() is not designed for safe insertion into HTML.
It is correct as JavaScript syntax,
but it does not account for the HTML context, where <script> can be closed prematurely.
What exactly is dangerous
| Dangerous sequence | Why | Example |
|---|---|---|
</script> | Closes the <script> tag | "</script><script>alert(1)</script>" |
<!-- or --> | Can start or close an HTML comment | "<!--evil-->" |
<script, <img, onerror= | Can initiate a new tag | "<img src=x onerror=alert(1)>" |
The characters </, <, > | Break the HTML structure | "Hello </div>" |
How to do it correctly
1. Safely escape the JSON before inserting it
An example (Node.js / Express / Next.js):
const safeJson = JSON.stringify(data)
.replace(/</g, '\\u003C')
.replace(/>/g, '\\u003E')
.replace(/&/g, '\\u0026')
.replace(/
/g, '\\u2028')
.replace(/
/g, '\\u2029');
const html = `
<script>
window.__INITIAL_DATA__ = ${safeJson};
</script>
`;Now even if a user enters </script>,
the browser gets </script>,
which does not close the tag and does not run the script.
2. Use ready-made utilities
Frameworks like Next.js or Nuxt do this automatically:
// Next.js example
<NextScript />
// generates a safe insertion of initialProps, with special characters escapedIf you write SSR by hand, use ready-made libraries:
serialize-javascriptsafe-json-stringify
import serialize from 'serialize-javascript';
const html = `<script>window.__DATA__ = ${serialize(data, { isJSON: true })}</script>`;3. Isolate data from code
It is better not to insert JSON directly into <script>,
but to hand it over, for example, through <script type="application/json">:
<script id="initial-data" type="application/json">
{"user": {"name": "Tim"}}
</script>And on the client:
const data = JSON.parse(document.getElementById('initial-data').textContent);This way the browser never executes the content of this script as JS code.
Short summary
| What | Description |
|---|---|
| Problem | JSON.stringify() inserts "raw" JSON into HTML, enabling XSS |
| Threat | An attacker can inject <script> or other dangerous tags |
| Solution | Escape <, >, &,
,
|
| Alternative | Use serialize-javascript or <script type="application/json"> |
Conclusion
During SSR:
Never insert the result of
JSON.stringify()directly into HTML Always escape special characters or use safe serializers
This is one of the most common XSS vulnerabilities in SSR applications,
and it can be prevented with a single line: .replace(/</g, '\\u003C').
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.