JSON Toolkit

Как через JSON происходит XSS

Главное: 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;    // parses markup
el.textContent = user.bio;  // inserts text

{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, разбирается идентично и больше не может закрыть тег.

Дополнительное чтение