Безопасность React приложений: что защищает React, а что должна делать команда

Подробный разбор безопасности React приложений: XSS, токены, CSP, third-party скрипты, архитектурные границы, типичные ошибки в production и сильные ответы для интервью.

24 апреля 2026 г.19 минLexicon Team

Введение

Безопасность React приложений часто обсуждают слишком узко: либо как разговор только про dangerouslySetInnerHTML, либо как спор о том, где хранить токен. На практике инциденты почти всегда возникают на стыке нескольких решений: как рендерится контент, кому доверяет frontend, где проходит граница между клиентом и сервером, какие скрипты загружаются в страницу и насколько команда понимает модель угроз браузера. Если нужен отдельный глубокий разбор именно XSS-угла, он уже вынесен в React security: XSS и защита в production-проектах.

Хорошая инженерная позиция здесь такая: React не делает приложение безопасным автоматически, но помогает избежать некоторых базовых ошибок, если команда не ломает его модель рендера своими руками. Дальше безопасность строится слоями: безопасный вывод данных, серверная валидация, ограничения для браузера, аккуратная работа с аутентификацией, контроль сторонних интеграций и регулярная проверка критических сценариев.

В этой статье разберем безопасность React приложений не как список случайных советов, а как систему решений: что защищено по умолчанию, где защита исчезает, как проектировать границы ответственности, какие ошибки чаще всего попадают в production и как про это уверенно говорить на интервью.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью

Подписаться

Что React защищает по умолчанию, а что нет

Самая полезная встроенная гарантия React проста: если вы рендерите значение как текст в JSX, библиотека экранирует специальные символы и не дает им превратиться в исполняемый HTML.

type CommentProps = {
  text: string;
};

export function Comment({ text }: CommentProps) {
  return <p>{text}</p>;
}

Если text пришел как "<img src=x onerror=alert(1)>", пользователь увидит строку как текст - обработчик не выполнится. Для значительной части интерфейса этого уже достаточно.

Проблемы начинаются там, где команда выходит за пределы обычного JSX-рендера:

  • вставляет HTML через dangerouslySetInnerHTML;
  • рендерит markdown или rich text без строгой фильтрации;
  • собирает href и src из недоверенных данных;
  • доверяет location.search, hash, postMessage или localStorage как безопасному источнику;
  • считает, что client-side guard равен настоящей авторизации.

React также не решает:

  • хранение и отзыв токенов;
  • проверку прав доступа;
  • CSRF для cookie-based модели;
  • безопасность API и схем данных;
  • контроль third-party скриптов;
  • политику заголовков вроде CSP, X-Frame-Options, Referrer-Policy.

Именно поэтому ответ "у нас React, значит XSS уже закрыт" почти всегда сигнал поверхностного понимания.

Архитектура безопасности: где проходит граница ответственности

Сильная модель безопасности в React-приложении строится не вокруг одного хука или одной библиотеки, а вокруг понятных границ между слоями.

Компоненты системы

Рабочая схема обычно выглядит так:

  1. Backend определяет, какие данные вообще могут содержать HTML, какие поля считаются недоверенными и какие контракты допустимы.
  2. API возвращает либо безопасный текст, либо структурированные данные, либо строго ограниченный HTML после серверной очистки.
  3. Frontend по умолчанию рендерит все как текст и использует HTML только в явно выделенных точках.
  4. Для редких HTML-сценариев есть отдельный безопасный компонент-обертка, а не разбросанные по всему проекту вызовы dangerouslySetInnerHTML.
  5. Браузерные ограничения через CSP и security headers уменьшают последствия инъекции и ошибок интеграции.
  6. Логи, Sentry и E2E-проверки ловят аномалии до того, как инцидент станет массовым.

Поток данных и контрольных точек

Представим страницу профиля, где есть поле bio, аватар, список подключенных виджетов и панель аккаунта:

  1. Пользователь отправляет данные в форму.
  2. Backend валидирует схему, отсекает лишние поля и сохраняет безопасный формат.
  3. API отдает клиенту только нужный набор свойств.
  4. React-компоненты рендерят имя, описание и статусы как текст.
  5. Если блок допускает rich text, он проходит через отдельный sanitizer и allowlist.
  6. Авторизация и права проверяются сервером, а клиент лишь отражает текущее состояние UI.

Критическая мысль здесь одна: безопасность теряется там, где система слишком рано считает данные "уже проверенными". Этот же подход полезно использовать при знакомстве с архитектурой больших React-приложений, потому что хаос в слоях почти всегда приводит к хаосу в security-решениях.

Точка отказа и стратегия деградации

Самая опасная точка отказа в React-проекте обычно не "плохой компонент", а отсутствие единого безопасного пути. Если один экран использует sanitizer, второй вставляет HTML напрямую, а третий доверяет CMS без контракта, инцидент становится вопросом времени.

Нормальная стратегия деградации такая:

  • если HTML не обязателен, хранить и рендерить текст;
  • если HTML обязателен, ограничить допустимый набор тегов и атрибутов;
  • если интеграция слишком рискованная, изолировать ее через iframe или вынести за пределы основного приложения.

Главные зоны риска в React-приложениях

XSS и небезопасный рендер

XSS остается самой обсуждаемой темой не случайно: именно она делает последствия любой другой ошибки более серьёзными. Если приложение хранит токены в доступном JavaScript-хранилище, то успешный XSS сразу поднимает цену инцидента.

import DOMPurify from "dompurify";

type RichTextProps = {
  html: string;
};

export function SafeRichText({ html }: RichTextProps) {
  const sanitized = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["p", "strong", "em", "ul", "ol", "li", "a", "code", "pre"],
    ALLOWED_ATTR: ["href", "target", "rel"],
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

Даже здесь важно не обманывать себя: sanitizer полезен, но он не должен становиться оправданием для бездумного рендера произвольного HTML. Чем реже в приложении вообще нужен HTML, тем проще защищать систему.

Токены, сессии и ложная безопасность client-side auth

Одна из самых частых ошибок frontend-команд звучит так: "мы спрятали route, значит пользователь не пройдет". Это UX-ограничение, а не полноценная защита. Клиент может скрыть экран, но не должен быть единственной точкой принятия решения о доступе.

Вариантов хранения auth-данных обычно два:

  • httpOnly cookie плюс серверная сессия или refresh-flow;
  • токен в JavaScript-доступном хранилище вроде memory store или localStorage.

У обоих подходов есть компромиссы. Cookie-модель лучше скрывает токен от JavaScript, но требует аккуратной CSRF-защиты и SameSite. Хранение в localStorage проще для SPA, но делает XSS намного дороже. Сильный инженерный ответ не выбирает "модный" вариант, а объясняет модель угроз конкретного продукта.

Third-party скрипты и партнерские виджеты

На живых проектах уязвимость часто возникает не из-за собственного UI, а через чат, аналитику, A/B-платформу, виджет оплаты или маркетинговый embed. Команда добавляет один <script>, а затем удивляется, почему CSP невозможно ужесточить без поломки страницы.

Практическое правило:

  • все внешние скрипты должны иметь владельца внутри команды;
  • список доменов для загрузки должен быть минимальным;
  • рискованные интеграции лучше изолировать, чем пускать в общий DOM без ограничений.

Формы, API и доверие к данным

React-форма не делает данные валидными сама по себе. Любая проверка только на клиенте полезна для UX, но не может считаться окончательной. Пользователь, бот или сторонний клиент может отправить запрос к API, вообще не пользуясь вашим интерфейсом.

Поэтому нужны оба слоя:

  • frontend валидирует формат, улучшает UX и предотвращает очевидные ошибки;
  • backend повторяет проверку, нормализует данные и отвергает лишние поля.

Этот же принцип полезен в сценариях с react-hook-form, file upload, ссылками, markdown и настройками профиля: источник истины в безопасности не должен оставаться только на клиенте.

Сравнение подходов: что реально снижает риск

ПодходЧто закрываетКогда полезенОграничение
Обычный JSX-рендер текстаБазовые XSS через текстовый выводБольшая часть UIНе подходит для HTML-контента
Санитизация HTMLУдаляет опасные теги и атрибутыCMS, rich text, markdownТребует жесткого allowlist и дисциплины
httpOnly cookieСнижает риск кражи токена через JSСессионные и hybrid auth-схемыНужны SameSite, CSRF-модель и серверная логика
Токен в memory storeУменьшает постоянство компрометацииКороткоживущие access token flowТеряется при reload и все равно уязвим к XSS в рамках сессии
CSP и security headersРежут часть векторов выполненияProduction и внешние интеграцииНе исправляют небезопасный код
Изоляция third-party через iframeОграничивает влияние чужого кодаВиджеты и партнерские вставкиУсложняет интеграцию и обмен данными
Серверная валидация и нормализацияЗакрывает доверие к клиентуЛюбые формы и APIНе спасает от небезопасного клиентского рендера

Практический вывод обычно такой: лучший security-эффект дает не одна "волшебная" мера, а связка из безопасного рендера, server-side проверки, ограничений браузера и контроля интеграций.

Прокачай React за 7 дней

20 вопросов и разборов по React Hooks

Начать

Ошибки в продакшене: где команды реально ошибаются

1. Безопасный код есть, но один компонент, обходящий безопасность, ломает всё

Признак в кодовой базе простой: почти везде текст рендерится нормально, но есть один "универсальный renderer", который принимает сырой HTML. В логах это обычно никак не видно до первого инцидента. Исправление одно: вынести такой путь в явный безопасный компонент и ограничить места использования через ревью и lint-правила.

2. Client-side auth принимают за настоящую авторизацию

Симптом: пользователь не видит кнопку, но все еще может вызвать чувствительный endpoint напрямую. В метриках это проявляется как неожиданные 403, всплески невалидных запросов или жалобы на "скрытые, но доступные" действия. Лечение: сервер всегда проверяет права сам, а React только отражает допустимый UI.

3. CSP добавляют слишком поздно

Обычно это происходит после того, как приложение уже обросло десятком внешних доменов, inline-скриптами и хаотичными вставками. Тогда любая строгая политика ломает полсайта. Гораздо дешевле проектировать CSP рано и постепенно ужесточать allowlist, чем чинить ее в момент аудита.

4. Проверяют только happy path

Команда тестирует логин, профиль и покупки, но не проверяет, что произойдёт при неожиданном HTML, некорректном редиректе, вредном query-параметре или измененном postMessage. Именно такие сценарии чаще всего и вскрывают слабые места. Здесь хорошо помогает связка с E2E тестированием React приложений, когда security-кейсы входят в набор критических пользовательских путей, а не живут только в головах разработчиков.

Разбор производительности: где security-меры стоят дорого, а где нет

У безопасности React-приложений тоже есть цена, но команды часто ошибаются в том, где именно она возникает.

Сами по себе:

  • экранирование текста React почти ничего не стоит;
  • security headers не создают заметной клиентской нагрузки;
  • проверка прав на сервере обычно дешевле, чем последствия неверного доступа.

Дороже обходятся:

  • тяжелая санитизация больших HTML-блоков на каждый рендер;
  • избыток внешних скриптов, которые грузят main thread;
  • лишняя криптография или auth-обвязка в горячем клиентском пути;
  • постоянные проверки и трансформации данных внутри рендера вместо нормализации на границе.

Если HTML-контент большой и обновляется часто, санитизацию лучше делать:

  1. как можно ближе к точке получения данных;
  2. один раз на изменение, а не на каждый лишний ререндер;
  3. по возможности на сервере или в предобработке, если контракт это допускает.

Это хороший пример инженерного компромисса: безопасность не должна превращаться в оправдание для медленного интерфейса, но и performance не оправдывает небезопасный рендер.

Практики, которые реально повышают безопасность

const ALLOWED_PROTOCOLS = new Set(["https:", "mailto:"]);

export function SafeLink({
  href,
  children,
}: {
  href: string;
  children: React.ReactNode;
}) {
  let safeHref = "#";

  try {
    const url = new URL(href, "https://lexiconium.ru");
    if (ALLOWED_PROTOCOLS.has(url.protocol)) {
      safeHref = url.toString();
    }
  } catch {
    safeHref = "#";
  }

  return (
    <a href={safeHref} rel="noopener noreferrer">
      {children}
    </a>
  );
}

Полезный baseline для большинства React-команд выглядит так:

  • по умолчанию рендерить недоверенные данные только как текст;
  • не разбрасывать dangerouslySetInnerHTML, а завернуть его в один безопасный компонент;
  • явно договориться, где хранятся токены и какая модель угроз у продукта;
  • держать CSP, Referrer-Policy, X-Frame-Options и похожие заголовки как часть платформы, а не как разовый аудит;
  • проверять внешние скрипты так же строго, как внутренние зависимости;
  • добавлять security-кейсы в E2E и smoke-набор;
  • логировать отклоненные redirect-URL, ошибки sanitizer и неожиданные схемы данных;
  • планировать rollout security-изменений постепенно, чтобы не ломать production внезапным ужесточением политики.

Частые ошибки

  • Считать, что React полностью закрывает XSS сам по себе.
  • Объяснять безопасность только через localStorage vs cookie, игнорируя остальную систему.
  • Скрывать кнопки на клиенте и называть это авторизацией.
  • Санитизировать контент хаотично, а не в одном контролируемом месте.
  • Подключать third-party скрипты без владельца, allowlist и стратегии отката.
  • Доверять данным из query string, hash и postMessage как "своим".
  • Тестировать только happy path и не проверять деградацию на вредных входах.

Как отвечать на интервью

Сильный ответ на вопрос про безопасность React приложений обычно строится так:

  1. React безопасно экранирует текст в JSX, но это только один слой защиты.
  2. Основные риски в React-приложениях: XSS, хранение токенов, ложная client-side авторизация, third-party скрипты и доверие к данным из API и браузерной среды.
  3. Защита должна быть слоистой: безопасный рендер, серверная валидация, корректная auth-модель, CSP и наблюдаемость.
  4. Компромиссы зависят от продукта: например, cookie уменьшают риск кражи токена через JS, но требуют CSRF-защиты; localStorage упрощает SPA-поток, но делает XSS дороже.
  5. Хорошая практика не в том, чтобы назвать пять buzzword-терминов, а в том, чтобы показать границы ответственности между frontend и backend.

Если хочется выделиться среди кандидатов уровня middle, полезно добавить реальный критерий выбора: "в нашем проекте rich text был только в одном модуле, поэтому мы вынесли его в отдельный безопасный renderer, запретили сырой HTML в остальных экранах и поэтапно внедряли CSP, чтобы не сломать внешние интеграции".

Потренируйте React-собеседования на реальных инженерных кейсах

Практика по React с вопросами про security, архитектурные границы, производительность, state management и аргументацию решений без шаблонных ответов

Начать практику

FAQ

Правда ли, что XSS в React бывает только через dangerouslySetInnerHTML?

Нет. Это самый известный путь, но не единственный. Проблемы также возникают через небезопасные URL, markdown без фильтрации, DOM-based сценарии, postMessage, доверие к данным из браузерного окружения и сторонние виджеты.

Универсального победителя нет. Cookie снижают доступность токена для JavaScript, но требуют аккуратной защиты от CSRF и продуманной серверной модели. localStorage проще, но успешный XSS делает такой выбор существенно дороже. Надо выбирать под модель угроз, а не по привычке.

Нужно ли проверять права на клиенте, если сервер и так все валидирует?

Да, но по другой причине. Клиентская проверка улучшает UX: скрывает недоступные действия, показывает правильные статусы и экономит лишние запросы. Решение о доступе все равно остается на сервере.

Помогает ли CSP, если в коде уже есть XSS?

Помогает уменьшить последствия и закрыть часть векторов выполнения, но не устраняет саму уязвимость. CSP полезна как дополнительный слой, а не как замена безопасного кода.

Какие security-проверки стоит автоматизировать в React-проекте в первую очередь?

Критичные auth-пути, redirect-сценарии, работу с недоверенным контентом, загрузку внешних скриптов, поведение защищенных экранов при смене роли и базовые negative-case E2E для пользовательского ввода.

Итоги

Безопасность React приложений начинается не с паники вокруг одного API, а с понимания границ. React помогает безопасно рендерить текст, но не берет на себя всю модель угроз приложения. Как только в системе появляются HTML, токены, внешние скрипты, rich text, авторизация и межсервисные контракты, защита становится архитектурной задачей.

Хороший security-уровень в React-команде выглядит довольно приземленно: недоверенные данные идут как текст, опасные сценарии собраны в контролируемые компоненты, права проверяются сервером, CSP и заголовки живут в платформе, а критические пути регулярно проверяются тестами. Такой подход не звучит эффектно, зато именно он выдерживает эксплуатацию в production.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью

Подписаться

Автор

Lexicon Team

Читайте также