JSON.stringify() і його небезпека
Проблема: впровадження неекранованого JSON в HTML
Під час SSR часто потрібно передати дані з сервера на клієнт, щоб React (або інший фреймворк) міг "гідрувати" сторінку, не завантажуючи їх повторно.
Типовий приклад
// На сервері:
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, без екранування.
Приклад користувацького вводу:
const data = {
user: { name: '</script><script>alert("XSS!")</script>' }
};Після JSON.stringify(data) ми отримаємо:
<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):
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 роблять це автоматично:
// приклад Next.js
<NextScript />
// генерує безпечну вставку initialProps, з екрануванням спецсимволівЯкщо пишеш SSR вручну - використовуй готові бібліотеки:
serialize-javascriptsafe-json-stringify
import serialize from 'serialize-javascript';
const html = `<script>window.__DATA__ = ${serialize(data, { isJSON: true })}</script>`;3. Ізолювати дані від коду
Краще не вставляти JSON напряму в <script>,
а віддавати його, наприклад, через <script type="application/json">:
<script id="initial-data" type="application/json">
{"user": {"name": "Tim"}}
</script>А на клієнті:
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').
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.