Безопасность в React: защита от XSS, CSRF и утечек данных

06 ноября 2026
Автор

Олег Марков

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

Встроенная защита React от XSS

React автоматически экранирует все значения, которые вставляются в JSX. Это защищает от большинства XSS-атак:

// ✅ React экранирует это автоматически
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>;
// Рендерится как текст, не как HTML
// Результат: <div>&lt;script&gt;alert("xss")&lt;/script&gt;</div>

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

Курс по теме

Курс React и Redux Toolkit с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Опасный dangerouslySetInnerHTML

Пропс dangerouslySetInnerHTML отключает встроенную защиту React. Используйте его только когда это действительно необходимо.

// ❌ Опасно — уязвимость XSS
function BlogPost({ content }: { content: string }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

// Если content пришёл от пользователя, это XSS-уязвимость
<BlogPost content="<img src=x onerror='alert(document.cookie)'>" />

Если нужно отображать HTML (например, из CMS или редактора), обязательно санируйте его:

npm install dompurify
npm install @types/dompurify  # для TypeScript
import DOMPurify from 'dompurify';

// ✅ Санируем HTML перед вставкой
function BlogPost({ content }: { content: string }) {
  const sanitizedContent = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'h2', 'h3'],
    ALLOWED_ATTR: [],
  });

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

Настраивайте ALLOWED_TAGS минимально — разрешайте только те теги, которые действительно нужны.

Безопасные ссылки и URL

Никогда не подставляйте URL из ненадёжных источников в href без проверки:

// ❌ Опасно — javascript: URL выполнит код
function UserLink({ user }: { user: User }) {
  return <a href={user.profileUrl}>{user.name}</a>;
}
// Если user.profileUrl = "javascript:alert(document.cookie)" — выполнится XSS

// ✅ Проверяем протокол URL
function sanitizeUrl(url: string): string {
  try {
    const parsed = new URL(url);
    // Разрешаем только http и https
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return '#';
    }
    return url;
  } catch {
    return '#';
  }
}

function UserLink({ user }: { user: User }) {
  return <a href={sanitizeUrl(user.profileUrl)}>{user.name}</a>;
}

Переменные окружения: не выставляйте секреты

В Next.js и Create React App все переменные с префиксом NEXT_PUBLIC_ или REACT_APP_ попадают в клиентский bundle и видны всем пользователям.

// ❌ НИКОГДА не делайте так — секрет будет виден в браузере
const apiKey = process.env.NEXT_PUBLIC_SECRET_API_KEY;
const dbPassword = process.env.NEXT_PUBLIC_DB_PASSWORD;

// ✅ Переменные без NEXT_PUBLIC_ доступны только на сервере
// Используйте Server Actions или API routes
async function fetchProtectedData() {
  const apiKey = process.env.SECRET_API_KEY; // только серверный код
  const response = await fetch('https://api.example.com/data', {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  return response.json();
}

Проверяйте, что попадает в bundle через анализатор:

npm run build && npx @next/bundle-analyzer

Безопасное хранение токенов аутентификации

Выбор места хранения JWT-токена — важное решение безопасности:

Место храненияXSSCSRFРекомендация
localStorageУязвимБезопасенНе рекомендуется
sessionStorageУязвимБезопасенНе рекомендуется
Cookie (httpOnly)БезопасенУязвим (нужен CSRF-токен)Рекомендуется
// ✅ Рекомендуемый подход — httpOnly cookie устанавливается на сервере
// На клиенте нет прямого доступа к токену

// API route (Next.js) — устанавливает httpOnly cookie
export async function POST(request: Request) {
  const { email, password } = await request.json();
  const token = await authenticateUser(email, password);

  const response = NextResponse.json({ success: true });
  response.cookies.set('auth-token', token, {
    httpOnly: true,      // недоступен для JavaScript
    secure: true,        // только HTTPS
    sameSite: 'strict',  // защита от CSRF
    maxAge: 60 * 60 * 24 * 7, // 7 дней
  });

  return response;
}
// ❌ Плохой вариант — токен доступен любому JS-коду
localStorage.setItem('token', jwtToken);

// ❌ Тоже плохо
const token = document.cookie.replace(/(?:(?:^|.*;\s*)token\s*=\s*([^;]*).*$)|^.*$/, '$1');

Защита от CSRF

Если вы используете cookie для хранения токенов, нужна защита от CSRF:

// Генерируем CSRF-токен при загрузке страницы
function useCsrfToken() {
  const [csrfToken, setCsrfToken] = useState('');

  useEffect(() => {
    // Получаем CSRF-токен с сервера
    fetch('/api/csrf-token')
      .then(r => r.json())
      .then(({ token }) => setCsrfToken(token));
  }, []);

  return csrfToken;
}

// Включаем токен в каждый мутирующий запрос
function useApiMutation() {
  const csrfToken = useCsrfToken();

  const mutate = async (url: string, data: unknown) => {
    return fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken,
      },
      body: JSON.stringify(data),
    });
  };

  return { mutate };
}

Валидация данных на клиенте

Клиентская валидация улучшает UX, но никогда не является заменой серверной валидации:

import { z } from 'zod';

// Схема валидации
const registrationSchema = z.object({
  email: z.string().email('Некорректный email'),
  password: z
    .string()
    .min(8, 'Минимум 8 символов')
    .regex(/[A-Z]/, 'Нужна хотя бы одна заглавная буква')
    .regex(/[0-9]/, 'Нужна хотя бы одна цифра'),
  name: z.string().min(2).max(100),
});

type RegistrationData = z.infer<typeof registrationSchema>;

function RegistrationForm() {
  const handleSubmit = async (data: RegistrationData) => {
    // Валидация на клиенте (UX)
    const result = registrationSchema.safeParse(data);
    if (!result.success) {
      // Показываем ошибки
      return;
    }

    // Серверная валидация тоже обязательна!
    await registerUser(result.data);
  };
}

Защита от утечек чувствительных данных в UI

// ❌ Никогда не логируйте чувствительные данные
function LoginForm() {
  const handleSubmit = (data: LoginData) => {
    console.log('Submitting:', data); // Пароль виден в консоли!
    submitLogin(data);
  };
}

// ❌ Не отображайте полные данные карт
function PaymentInfo({ card }: { card: PaymentCard }) {
  return <span>{card.number}</span>; // Полный номер карты на экране!
}

// ✅ Маскируйте чувствительные данные
function PaymentInfo({ card }: { card: PaymentCard }) {
  const maskedNumber = `**** **** **** ${card.lastFour}`;
  return <span>{maskedNumber}</span>;
}

Content Security Policy

CSP — важный заголовок, ограничивающий источники ресурсов. В Next.js настраивается в next.config.js:

// next.config.js
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: [
              "default-src 'self'",
              "script-src 'self' 'unsafe-inline'", // убрать unsafe-inline если возможно
              "style-src 'self' 'unsafe-inline'",
              "img-src 'self' data: https:",
              "connect-src 'self' https://api.yourservice.com",
              "frame-src 'none'",
            ].join('; '),
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
        ],
      },
    ];
  },
};

Зависимости: регулярный аудит

# Проверка известных уязвимостей в зависимостях
npm audit

# Автоматическое исправление некритичных уязвимостей
npm audit fix

# Проверка устаревших пакетов
npm outdated

Настройте автоматический аудит в CI/CD:

# .github/workflows/security.yml
name: Security Audit
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm audit --audit-level=high

Итоги

Ключевые правила безопасности React-приложений:

  1. Избегайте dangerouslySetInnerHTML — если нужно, используйте DOMPurify
  2. Проверяйте URL перед вставкой в href или src
  3. Не выставляйте секреты через NEXT_PUBLIC_ переменные
  4. Храните токены в httpOnly cookie, а не в localStorage
  5. Валидируйте на сервере — клиентская валидация только для UX
  6. Маскируйте чувствительные данные в UI
  7. Настраивайте CSP для ограничения источников ресурсов
  8. Регулярно проводите npm audit

React защищает от XSS в JSX автоматически — но это только первый рубеж защиты.

Стрелочка влевоRender Props: гибкое управление рендерингом в ReactZustand: продвинутые паттерны, persist middleware и slicesСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

React тестирование с Vitest
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
Valtio: атомарный state management в ReactuseState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseInsertionEffect — внедрение стилей до мутаций DOMuseLayoutEffect в React — эффект до отрисовкиuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseFormStatus - отслеживание статуса отправки формыuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseContext — работа с контекстом в ReactuseActionState в React 19React Stale Closures — устаревшие замыкания в хукахОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияflushSync — синхронные обновления состояния в ReactКастомные хуки в React — создание собственных хуковuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?useState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Uncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactБезопасность в React: защита от XSS, CSRF и утечек данныхZustand: продвинутые паттерны, persist middleware и slicesРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native New Architecture: Fabric и JSIReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовАрхитектура больших React приложений: Feature-Sliced DesignuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий