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

16 июня 2026
Автор

Олег Марков

Безопасность в React

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-выражение.

Опасный 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

Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRecoil — библиотека управления состоянием от FacebookRedux Toolkit - современный ReduxВиртуализация списков с 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 - перетаскивание элементовАнимация при монтировании компонентов в ReactMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовМокирование APIОбработка ошибок API
Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг 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, семантика и клавиатурная навигация
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect 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 ₽
Подробнее

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