JSON Toolkit

Як XSS трапляється через JSON

Головне: JSON ніколи не виконується сам. XSS трапляється в момент, коли ви трактуєте його як HTML — склеювання рядків на сервері, `innerHTML` на фронтенді або вбудовування JSON просто в тег скрипта.

Середній~4 хв читанняОпубліковано 2026-10-30Оновлено 2026-10-30
Три способи, якими JSON стає XSS, і як їх запобігти

Коротко: JSON ніколи не виконується сам — спершу закрийте три шляхи виводу: екрануйте на сервері за контекстом, використовуйте textContent замість innerHTML і замінюйте < на \u003c під час вбудовування JSON.

{"name": "<img src=x onerror=alert(1)>"} — валідні дані. Чи небезпечні вони, цілком залежить від того, куди ви їх покладете.

Вхід перший: складання рядків на сервері

res.send(`<div>Hello, ${data.name}</div>`);

Браузер розбирає теги всередині data.name. JSON тут лише контейнер; провина — екранування для неправильного контексту.

Виправлення: використовуйте типове екранування рушія шаблонів (<%= %> в EJS, {{ }} у Vue й Django). Уникайте <%- %>, v-html і |safe для всього контрольованого користувачем.

Вхід другий: innerHTML

el.innerHTML = user.bio;    // розбирає розмітку
el.textContent = user.bio;  // вставляє текст

{user.bio} у React і {{ user.bio }} у Vue створюють текстові вузли й безпечні за замовчуванням. dangerouslySetInnerHTML і v-html — явні відмови від цього.

Якщо вам справді потрібен багатий текст, санітизуйте за білим списком (DOMPurify та подібні), а не довіряйте бекенду.

Вхід третій: вбудовування в тег скрипта

<script>
  window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>

</script> усередині даних передчасно закриває блок, і все після нього виконується як новий скрипт. Це найбільш проґавлений випадок.

JSON.stringify(data)
  .replace(/</g, '\\u003c')
  .replace(/>/g, '\\u003e')
  .replace(/&/g, '\\u0026');

Усе ще валідний JSON, ідентичний після розбору, більше не може закрити тег. Next.js і Nuxt роблять те саме для __NEXT_DATA__ і __NUXT__.

Два сусіди, варті знання

Викрадення JSON — старі браузери виконували JSON, завантажений через <script src>; перевизначення Array зливало вміст. Сучасні браузери це виправили, а Content-Type: application/json плюс X-Content-Type-Options: nosniff закривають решту.

Залишки JSONP — контрольована користувачем назва зворотного виклику за задумом є виконанням довільного коду. Не використовуйте його в нових системах.

Чек-лист

  • Кожне місце, де поле JSON досягає HTML, екранується для свого контексту (HTML, атрибут, JS, URL)
  • Жоден innerHTML, v-html чи dangerouslySetInnerHTML не споживає рядки API
  • Вбудований JSON екранує <, > і &
  • Багатий текст проходить санітизацію за білим списком, а не фільтрацію за чорним
  • Відповіді несуть Content-Type: application/json і nosniff

Часті питання

Чому валідний JSON усе ще може спричинити XSS?

JSON — лише текст і ніколи не виконується; бути валідним означає лише розбиратися. Небезпека зʼявляється, коли його пишуть у контекст HTML або скрипта: браузер розбирає теги, або </script> передчасно закриває блок, і решта виконується як код. Провина — відсутнє контекстно-залежне екранування під час виводу, а не формат даних.

Використовувати textContent чи innerHTML для відображення полів API?

Надавайте перевагу textContent: він вставляє значення як звичайний текст, тож теги показуються буквально. innerHTML розбирає розмітку, тож поле API, що містить <img onerror=...>, виконується. {user.bio} у React і {{ user.bio }} у Vue типово створюють текстові вузли; dangerouslySetInnerHTML і v-html — явні відмови.

Як екранувати JSON, вбудований у тег <script>?

Так. </script> усередині даних передчасно закриває блок, і все після нього виконується як новий скрипт. Після серіалізації замініть <, > і & на \u003c, \u003e і \u0026: результат усе ще валідний JSON, розбирається ідентично й більше не може закрити тег.

Додаткове читання