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

22 сентября 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-токена — важное решение безопасности:

Место хранения XSS CSRF Рекомендация
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 автоматически — но это только первый рубеж защиты.

Стрелочка влевоUncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactСтрелочка вправо

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

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

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

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

Все гайды по React

React тестирование с Vitest
React с TanStack Router
Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Testing LibraryReact Toastify - уведомления в ReactСоздание таблиц в 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)Загрузка и индикаторыJotai - атомарное состояниеАнимация списков в ReactБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияflushSync — синхронные обновления состояния в ReactuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуков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
Открыть базу знаний

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

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

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 ₽
Подробнее

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