Skip to main content

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

javascript
// 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:

javascript
const data = { user: { name: '</script><script>alert("XSS!")</script>' } };

After JSON.stringify(data) we get:

javascript
<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 sequenceWhyExample
</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):

javascript
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:

javascript
// Next.js example <NextScript /> // generates a safe insertion of initialProps, with special characters escaped

If you write SSR by hand, use ready-made libraries:

  • serialize-javascript
  • safe-json-stringify
javascript
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">:

javascript
<script id="initial-data" type="application/json"> {"user": {"name": "Tim"}} </script>

And on the client:

javascript
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

WhatDescription
ProblemJSON.stringify() inserts "raw" JSON into HTML, enabling XSS
ThreatAn attacker can inject <script> or other dangerous tags
SolutionEscape <, >, &, 
, 

AlternativeUse 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.