Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Безопасность React-приложений
security

Безопасность React-приложений

XSS, опасный HTML, URL и точки записи в DOM, CSRF, токены, цепочка поставки зависимостей, CSP и недоверенные серверные действия.

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

Безопасность React-приложений

React безопасно экранирует обычный текст, но приложение остаётся браузерной системой с недоверенными данными, зависимостями и серверными командами.

#Результат урока

Вы построите модель угроз, закроете опасные места вывода и проверите серверную авторизацию, CSRF и риски цепочки поставки.

Понадобятся JSX, формы и действия, маршрутизация, серверные данные и модель безопасности браузера. Итог урока — разбор безопасности комментариев с очисткой HTML, безопасными URL, сеансом через BFF и регрессионными тестами.

#1. Контексты вывода и экранирование

Обычная строка в тексте или атрибуте JSX экранируется. Но у HTML, URL, CSS и скрипта разные правила безопасности. Не заменяйте < вручную; используйте безопасный API и проверку по списку разрешённого (allowlist).

Одна и та же строка ведёт себя по-разному в зависимости от места вывода:

const value = '<img src=x onerror="alert(1)">'; <p>{value}</p> // ✅ выводится как текст, тег не создаётся <div dangerouslySetInnerHTML={{ __html: value }} /> // ❌ создаётся элемент, срабатывает onerror <a href={value}>ссылка</a> // ❌ другой контекст, другие правила

Экранирование React работает только для текстового контекста и значений атрибутов. Функция, заменяющая < на &lt;, ничем не поможет в контексте URL: javascript:alert(1) не содержит угловых скобок вообще. Поэтому правило одно — под каждый контекст свой безопасный API.

Проверьте себя. Передайте вредоносную строку как текст и в опасное место вывода, зафиксируйте разницу.

Частая ошибка. Считать одну функцию экранирования подходящей для всех контекстов.

#2. Опасный HTML и очистка

dangerouslySetInnerHTML обходит экранирование React. Используйте его только для осознанного HTML после очистки (sanitization) по списку разрешённого, настроенному под конкретную модель угроз. Очищенное значение нельзя затем изменять небезопасными расширениями. Trusted Types и CSP усиливают эту границу.

Очистка выполняется библиотекой с явным списком разрешённых тегов и атрибутов:

// src/features/comments/CommentBody.tsx const safeHtml = useMemo( () => DOMPurify.sanitize(markdownToHtml(comment.body), { ALLOWED_TAGS: ['p', 'a', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li'], ALLOWED_ATTR: ['href', 'title'], }), [comment.body], ); return <div dangerouslySetInnerHTML={{ __html: safeHtml }} />;

Самодельная проверка регулярным выражением закрывает один вектор из сотни:

// ❌ проходит <img src=x onerror=...>, <svg onload=...>, вложенные и закодированные варианты const unsafe = html.replace(/<script[\s\S]*?<\/script>/gi, '');

Порядок шагов важен: сначала преобразование Markdown в HTML, затем очистка. Если очистить сначала, а потом дописать разметку — например, добавить подсветку кода небезопасным расширением, — гарантия теряется.

Проверьте себя. Очистите HTML из Markdown и проверьте нагрузки с img, onerror и svg.

Частая ошибка. Regex удаляет только <script>.

#3. URL и список разрешённой навигации

Недоверенный href проверяют через URL и список разрешённых схем и узлов. Для внутренней навигации ограничивают источник и путь; адрес перенаправления не принимает произвольный внешний URL. javascript: и опасные data: исключаются. Текст ссылки должен осмысленно описывать назначение.

Проверка внешней ссылки — разбор через URL и сравнение схемы со списком:

// src/shared/security/url.ts const SAFE_SCHEMES = new Set(['http:', 'https:', 'mailto:']); export function toSafeHref(value: string): string | null { try { const url = new URL(value, window.location.origin); return SAFE_SCHEMES.has(url.protocol) ? url.toString() : null; } catch { return null; // не разобралось — значит не ссылка } }

Внутреннее перенаправление проверяют иначе: не «какая схема», а «остались ли мы у себя»:

export function toInternalPath(value: string): string { const url = new URL(value, window.location.origin); // проверяем именно origin: строка "//evil.com/x" разбирается как внешний адрес return url.origin === window.location.origin ? url.pathname + url.search : '/'; }

Проверка вида value.startsWith('/') пропускает //evil.com/x, а JavaScript: с заглавными буквами и %6a%61vascript: обходят сравнение подстрок. Разбор через URL нормализует все эти варианты.

Проверьте себя. Проверьте закодированную схему и URL, начинающийся с //.

Частая ошибка. href={userValue} без правила проверки.

#4. Токен сеанса и выбор между рисками XSS и CSRF

Токен в localStorage доступен JavaScript и может быть украден при XSS. Cookie с HttpOnly скрыта от JavaScript, но браузер отправляет её автоматически, поэтому нужны SameSite и защита от CSRF. Выбор зависит от архитектуры; не записывайте учётные данные в журнал и не помещайте секреты в клиентский пакет.

Сравнение удобно держать перед глазами при проектировании сеанса:

ХранениеКрадётся при XSSОтправляется автоматическиЧто обязательно
localStorageда, любым скриптомнетстрогая CSP, очистка вывода
Cookie HttpOnlyнетдаSameSite, защита от CSRF, Secure

Для варианта с cookie сервер-посредник (BFF) выставляет её сам, и клиентский код токена вообще не видит:

// BFF: браузер получает только cookie, токен провайдера остаётся на сервере response.cookies.set('session', sessionId, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 60 * 60 * 8, });

Подпись JWT доказывает, что содержимое не подменяли, — и ничего больше. Она не мешает украденному токену работать до истечения срока и не отзывает его. Поэтому вопрос «где хранить» решают вместе с вопросами «как отозвать» и «как коротко живёт».

Проверьте себя. Нарисуйте модель угроз сеанса BFF на cookie.

Частая ошибка. Считать JWT безопасным только из-за подписи.

#5. CSRF и контракт изменения данных

Изменение данных с cookie-защитой требует SameSite, токена CSRF или специального заголовка и проверки Origin или Fetch Metadata — согласно архитектуре. GET не изменяет состояние. CORS не является полной защитой от CSRF: браузер умеет отправлять простые запросы и формы. Серверное действие остаётся публичной границей, подобной конечной точке API.

Проверка источника выполняется на сервере до всякой предметной логики:

// app/api/tickets/route.ts export async function POST(request: Request) { const origin = request.headers.get('origin'); const site = request.headers.get('sec-fetch-site'); // Fetch Metadata if (site !== 'same-origin' && origin !== process.env.APP_ORIGIN) { return new Response('Forbidden', { status: 403 }); } // дальше — аутентификация, разбор схемы, предметная операция }

Изменение состояния по GET открывает атаку без всякой формы: достаточно <img src="https://app/tickets/42/delete"> на чужом сайте, и браузер отправит cookie сам. CORS здесь не спасает: он ограничивает чтение ответа, а не отправку запроса — межсайтовая форма уйдёт на сервер, и запись произойдёт до того, как браузер откажет в чтении ответа.

Проверьте себя. Попробуйте межсайтовую отправку формы методом POST и проверьте отказ.

Частая ошибка. Изменение данных по ссылке с методом GET.

#6. Серверные функции как публичная поверхность

Экспортированную серверную функцию клиент может вызвать через протокол фреймворка. В каждой команде проверяйте аутентификацию, право на конкретный объект, схему, частоту, идемпотентность и безопасные ошибки. Скрытая кнопка не ограничивает доступ. Передавайте идентификаторы, а данные перечитывайте на сервере.

Скрытое поле формы — это ввод пользователя, ничем не отличающийся от текстового:

// ❌ app/actions.ts — userId и amount приходят из браузера 'use server'; export async function transfer(formData: FormData) { const userId = formData.get('userId') as string; // подменяется в DevTools const amount = Number(formData.get('amount')); await db.transfer(userId, amount); }

Правильная форма: личность берём из сеанса, права проверяем на конкретный объект, значения перечитываем с сервера:

// ✅ app/actions.ts 'use server'; export async function transfer(formData: FormData) { const session = await requireSession(); // кто это const parsed = TransferSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: 'Некорректные данные' }; const account = await db.accounts.find(parsed.data.accountId); if (account?.ownerId !== session.userId) { // право на этот объект return { error: 'Операция недоступна' }; // без деталей о чужих данных } await db.transfer(account.id, parsed.data.amount); }

Сообщение об ошибке тоже часть границы: «счёт принадлежит другому пользователю» подтверждает существование объекта и помогает перебирать идентификаторы.

Проверьте себя. Вызовите действие вручную с чужим recordId.

Частая ошибка. Доверять userId из скрытого поля.

#7. Цепочка поставки и сторонние скрипты

Зависимости выполняют код при установке, сборке и работе приложения. Файл блокировки (lockfile) и контрольные суммы дают воспроизводимость, но не безопасность. Сокращайте число пакетов и скриптов, проверяйте уведомления об уязвимостях, фиксируйте версии критичных средств и обновляйте с тестами. Сторонний скрипт получает права источника; песочница, согласие пользователя и CSP ограничивают риск.

Установка с проверкой файла блокировки и без исполнения сценариев пакетов — базовая гигиена в CI:

npm ci --ignore-scripts # ставим ровно то, что в lockfile, без postinstall npm audit --audit-level=high

Файл блокировки гарантирует, что вы поставите тот же код, что и в прошлый раз, — включая тот случай, когда этот код уже скомпрометирован. Отдельный вопрос — сторонние скрипты на странице: подключённый аналитический тег работает с правами вашего источника и читает DOM целиком, включая введённые в форму данные. Ограничить его можно только внешними средствами: iframe с песочницей, отложенная загрузка после согласия, узкая CSP.

Проверьте себя. Составьте перечень зависимостей и источников скриптов.

Частая ошибка. Добавить пакет ради тривиальной вспомогательной функции.

#8. CSP, clickjacking и безопасные заголовки

CSP ограничивает источники скриптов, стилей, соединений и фреймов и снижает последствия XSS; одноразовое значение (nonce) или хеш лучше широкого unsafe-inline. frame-ancestors защищает от подмены кликов (clickjacking). HSTS, nosniff, политика referrer и permissions дополняют защиту. CSP не заменяет очистку данных и авторизацию.

Внедрять политику начинают в режиме отчётов, чтобы увидеть реальные нарушения, ничего не сломав:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; report-uri /api/csp-report

После разбора отчётов заголовок переводят в блокирующий режим. Обратный порядок действий — добавлять разрешения, пока не исчезнут ошибки в консоли, — заканчивается политикой script-src 'self' 'unsafe-inline' 'unsafe-eval', которая не мешает ни одному из известных вариантов XSS и существует только для отчётности.

Проверьте себя. Составьте CSP только для отчётов и разберите нарушения.

Частая ошибка. Добавить unsafe-inline/unsafe-eval до исчезновения ошибок.

#Лаборатория

Найдите и исправьте хранимую XSS в предпросмотре Markdown, URL со схемой javascript:, токен в localStorage и журнале, изменение без защиты CSRF и открытое перенаправление. Добавьте план CSP, проверку зависимостей и тесты злоупотребления серверным действием.

Работа готова, когда недоверенные данные не исполняются, учётные данные без необходимости не доступны клиентскому JavaScript, а сервер проверяет каждое изменение.

#Источники для сверки

  • React dangerouslySetInnerHTML
  • OWASP Cross Site Scripting Prevention Cheat Sheet
  • OWASP CSRF Prevention
  • MDN CSP

Устранение неисправностей

Далее: SSR, потоковая передача и гидратация