Skip to main content

JSON.stringify() і його небезпека

Проблема: впровадження неекранованого JSON в HTML

Під час SSR часто потрібно передати дані з сервера на клієнт, щоб React (або інший фреймворк) міг "гідрувати" сторінку, не завантажуючи їх повторно.

Типовий приклад

javascript
// На сервері: 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);

На перший погляд - все ок. Але якщо data містить користувацькі дані, проблема не змусить себе чекати


Що може піти не так

Якщо всередині data є спеціальні символи HTML або JS, JSON.stringify() вставить їх як є в HTML, без екранування.

Приклад користувацького вводу:

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

Після JSON.stringify(data) ми отримаємо:

javascript
<script> window.__INITIAL_DATA__ = {"user":{"name":"</script><script>alert(\"XSS!\")</script>"}}; </script>

Що відбудеться:

  • браузер закриє перший <script> тег (</script>)
  • і виконає зловмисний скрипт alert("XSS!").

Вітаємо - класичний XSS через SSR.


Чому це відбувається

JSON.stringify() не призначений для безпечної вставки в HTML. Він коректний як JavaScript-синтаксис, але не враховує особливості HTML-контексту, де <script> може бути передчасно закритий.


Що конкретно небезпечно

Небезпечна послідовністьЧомуПриклад
</script>Закриває тег <script>"</script><script>alert(1)</script>"
<!-- або -->Може почати або закрити HTML-коментар"<!--evil-->"
<script, <img, onerror=Може ініціювати новий тег"<img src=x onerror=alert(1)>"
Символи </, <, >Порушують HTML-структуру"Hello </div>"

Як правильно

1. Безпечно екранувати JSON перед вставкою

Приклад (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> `;

Тепер навіть якщо користувач введе </script>, браузер отримає </script>, а це не закриє тег і не виконає скрипт.


2. Використовувати готові утиліти

Фреймворки на кшталт Next.js чи Nuxt роблять це автоматично:

javascript
// приклад Next.js <NextScript /> // генерує безпечну вставку initialProps, з екрануванням спецсимволів

Якщо пишеш SSR вручну - використовуй готові бібліотеки:

  • serialize-javascript
  • safe-json-stringify
javascript
import serialize from 'serialize-javascript'; const html = `<script>window.__DATA__ = ${serialize(data, { isJSON: true })}</script>`;

3. Ізолювати дані від коду

Краще не вставляти JSON напряму в <script>, а віддавати його, наприклад, через <script type="application/json">:

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

А на клієнті:

javascript
const data = JSON.parse(document.getElementById('initial-data').textContent);

Так браузер ніколи не виконає вміст цього скрипту як JS-код.


Короткий підсумок

ЩоОпис
ПроблемаJSON.stringify() вставляє "сирий" JSON в HTML, дозволяючи XSS
ЗагрозаЗловмисник може впровадити <script> або інші небезпечні теги
РішенняЕкранувати <, >, &, 
, 

АльтернативаВикористовувати serialize-javascript або <script type="application/json">

Висновок

При SSR:

Ніколи не вставляй результат JSON.stringify() напряму в HTML Завжди екрануй спецсимволи або використовуй безпечні серіалізатори

Це одна з найчастіших XSS-вразливостей у SSR-застосунках, її можна запобігти одним рядком .replace(/</g, '\\u003C').

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.