Безопасность React приложений: что защищает React, а что должна делать команда
Подробный разбор безопасности React приложений: XSS, токены, CSP, third-party скрипты, архитектурные границы, типичные ошибки в production и сильные ответы для интервью.
- Введение
- Что React защищает по умолчанию, а что нет
- Архитектура безопасности: где проходит граница ответственности
- Компоненты системы
- Поток данных и контрольных точек
- Точка отказа и стратегия деградации
- Главные зоны риска в React-приложениях
- XSS и небезопасный рендер
- Токены, сессии и ложная безопасность client-side auth
- Third-party скрипты и партнерские виджеты
- Формы, API и доверие к данным
- Сравнение подходов: что реально снижает риск
- Ошибки в продакшене: где команды реально ошибаются
- 1. Безопасный код есть, но один компонент, обходящий безопасность, ломает всё
- 2. Client-side auth принимают за настоящую авторизацию
- 3. CSP добавляют слишком поздно
- 4. Проверяют только happy path
- Разбор производительности: где security-меры стоят дорого, а где нет
- Практики, которые реально повышают безопасность
- Частые ошибки
- Как отвечать на интервью
- FAQ
- Правда ли, что XSS в React бывает только через dangerouslySetInnerHTML?
- Что безопаснее для React-приложения: cookie или localStorage?
- Нужно ли проверять права на клиенте, если сервер и так все валидирует?
- Помогает ли CSP, если в коде уже есть XSS?
- Какие security-проверки стоит автоматизировать в React-проекте в первую очередь?
- Итоги
Введение
Безопасность 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-приложении строится не вокруг одного хука или одной библиотеки, а вокруг понятных границ между слоями.
Компоненты системы
Рабочая схема обычно выглядит так:
- Backend определяет, какие данные вообще могут содержать HTML, какие поля считаются недоверенными и какие контракты допустимы.
- API возвращает либо безопасный текст, либо структурированные данные, либо строго ограниченный HTML после серверной очистки.
- Frontend по умолчанию рендерит все как текст и использует HTML только в явно выделенных точках.
- Для редких HTML-сценариев есть отдельный безопасный компонент-обертка, а не разбросанные по всему проекту вызовы
dangerouslySetInnerHTML. - Браузерные ограничения через CSP и security headers уменьшают последствия инъекции и ошибок интеграции.
- Логи, Sentry и E2E-проверки ловят аномалии до того, как инцидент станет массовым.
Поток данных и контрольных точек
Представим страницу профиля, где есть поле bio, аватар, список подключенных виджетов и панель аккаунта:
- Пользователь отправляет данные в форму.
- Backend валидирует схему, отсекает лишние поля и сохраняет безопасный формат.
- API отдает клиенту только нужный набор свойств.
- React-компоненты рендерят имя, описание и статусы как текст.
- Если блок допускает rich text, он проходит через отдельный sanitizer и allowlist.
- Авторизация и права проверяются сервером, а клиент лишь отражает текущее состояние 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-данных обычно два:
httpOnlycookie плюс серверная сессия или 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-контент большой и обновляется часто, санитизацию лучше делать:
- как можно ближе к точке получения данных;
- один раз на изменение, а не на каждый лишний ререндер;
- по возможности на сервере или в предобработке, если контракт это допускает.
Это хороший пример инженерного компромисса: безопасность не должна превращаться в оправдание для медленного интерфейса, но и 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 приложений обычно строится так:
- React безопасно экранирует текст в JSX, но это только один слой защиты.
- Основные риски в React-приложениях: XSS, хранение токенов, ложная client-side авторизация, third-party скрипты и доверие к данным из API и браузерной среды.
- Защита должна быть слоистой: безопасный рендер, серверная валидация, корректная auth-модель, CSP и наблюдаемость.
- Компромиссы зависят от продукта: например, cookie уменьшают риск кражи токена через JS, но требуют CSRF-защиты;
localStorageупрощает SPA-поток, но делает XSS дороже. - Хорошая практика не в том, чтобы назвать пять buzzword-терминов, а в том, чтобы показать границы ответственности между frontend и backend.
Если хочется выделиться среди кандидатов уровня middle, полезно добавить реальный критерий выбора: "в нашем проекте rich text был только в одном модуле, поэтому мы вынесли его в отдельный безопасный renderer, запретили сырой HTML в остальных экранах и поэтапно внедряли CSP, чтобы не сломать внешние интеграции".
Потренируйте React-собеседования на реальных инженерных кейсах
Практика по React с вопросами про security, архитектурные границы, производительность, state management и аргументацию решений без шаблонных ответов
FAQ
Правда ли, что XSS в React бывает только через dangerouslySetInnerHTML?
Нет. Это самый известный путь, но не единственный. Проблемы также возникают через небезопасные URL, markdown без фильтрации, DOM-based сценарии, postMessage, доверие к данным из браузерного окружения и сторонние виджеты.
Что безопаснее для React-приложения: cookie или localStorage?
Универсального победителя нет. 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
Читайте также
frontend
React security: XSS и защита в production-проектах
Разбираем React security на практике: откуда берется XSS, чем опасен dangerouslySetInnerHTML, как использовать DOMPurify, CSP и какие ошибки чаще всего допускают команды.
frontend
Frontend интервью: 18 вопросов по браузеру, сети, производительности и архитектуре
Опросник по frontend без повторов типичных React-вопросов: 18 тем по браузеру, сети, кэшу, безопасности, производительности, сборке, наблюдаемости и доставке.
frontend
E2E тестирование React приложений
Разбираем E2E тестирование React приложений: что проверять через Playwright, как строить сценарии, снижать flaky-сбои и уверенно отвечать на интервью.