Защита от CSRF

CSRF (Cross-Site Request Forgery) — атака, при которой злоумышленник заставляет браузер пользователя выполнить запрос к доверенному сайту от имени уже авторизованного пользователя.

Главная особенность CSRF заключается в том, что браузер автоматически прикрепляет к запросу:

  • cookies;
  • session id;
  • данные авторизации;
  • некоторые заголовки.

Если сервер не проверяет источник запроса и не использует защитные механизмы, злоумышленник может инициировать выполнение критических операций:

  • изменение профиля;
  • перевод денежных средств;
  • удаление данных;
  • смена пароля;
  • создание новых сущностей;
  • отправка форм.

Пример уязвимого запроса:

POST /api/user/email HTTP/1.1
Cookie: session=abc123
Content-Type: application/json

{
  "email": "attacker@example.com"
}

Если пользователь авторизован, браузер автоматически отправит cookie session=abc123, даже если запрос инициирован сторонним сайтом.


Почему CSRF особенно важен в TanStack Query

TanStack Query активно используется для:

  • мутаций (useMutation);
  • отправки POST/PUT/PATCH/DELETE запросов;
  • автоматического рефетчинга;
  • фоновой синхронизации;
  • централизованных API-клиентов.

В большинстве приложений TanStack Query работает совместно с:

  • fetch;
  • axios;
  • cookie-based authentication;
  • session authentication.

Именно cookie-based авторизация наиболее подвержена CSRF-атакам.

Если приложение использует:

Set-Cookie: session=...

то браузер будет автоматически прикреплять cookie ко всем запросам к домену.

Это создаёт потенциальную поверхность атаки.


Основные механизмы защиты

На практике защита строится на комбинации:

  • CSRF token;
  • SameSite cookies;
  • проверка Origin/Referer;
  • разделение доменов;
  • double submit cookie;
  • использование Authorization header вместо cookies.

Использование CSRF Token

Принцип работы

Сервер генерирует уникальный CSRF token и передаёт его клиенту.

Клиент обязан отправлять токен в каждом изменяющем запросе:

  • POST;
  • PUT;
  • PATCH;
  • DELETE.

Сервер сверяет токен с ожидаемым значением.

Если токен отсутствует или неверен — запрос отклоняется.


Получение CSRF токена

Часто сервер возвращает токен:

{
  "csrfToken": "a1b2c3d4"
}

Либо:

<meta name="csrf-token" content="a1b2c3d4">

Либо через cookie:

Set-Cookie: XSRF-TOKEN=a1b2c3

Интеграция CSRF с fetch

Базовый API-клиент

const csrfToken = document
  .querySelector('meta[name="csrf-token"]')
  ?.getAttribute('content')

export async function api(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken,
      ...options.headers
    }
  })

  if (!response.ok) {
    throw new Error('Request failed')
  }

  return response.json()
}

Использование в useMutation

import { useMutation } from '@tanstack/react-query'
import { api } from './api'

export function useUpdateProfile() {
  return useMutation({
    mutationFn: (data) =>
      api('/api/profile', {
        method: 'PATCH',
        body: JSON.stringify(data)
      })
  })
}

Теперь каждая мутация автоматически содержит CSRF token.


Централизованная защита

Почему защита должна быть глобальной

Ошибка многих приложений — ручное добавление CSRF token в отдельных местах:

headers: {
  'X-CSRF-Token': token
}

Такой подход приводит к:

  • дублированию;
  • забытым endpoints;
  • уязвимостям;
  • рассинхронизации;
  • сложному сопровождению.

Гораздо безопаснее использовать единый API layer.


Архитектура API Layer

Пример структуры

src/
  api/
    client.js
    csrf.js
    auth.js

csrf.js

let csrfToken = null

export function setCsrfToken(token) {
  csrfToken = token
}

export function getCsrfToken() {
  return csrfToken
}

client.js

import { getCsrfToken } from './csrf'

export async function api(url, options = {}) {
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers
  }

  const token = getCsrfToken()

  if (token) {
    headers['X-CSRF-Token'] = token
  }

  const response = await fetch(url, {
    ...options,
    credentials: 'include',
    headers
  })

  if (!response.ok) {
    throw new Error('API error')
  }

  return response.json()
}

Автоматическое обновление CSRF token

Некоторые серверы ротируют токены.

Например:

  • после логина;
  • после logout/login;
  • после refresh session;
  • после обновления access token.

В этом случае клиент обязан обновлять CSRF token.


Извлечение токена из response headers

export async function api(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    credentials: 'include'
  })

  const nextToken = response.headers.get('X-CSRF-Token')

  if (nextToken) {
    setCsrfToken(nextToken)
  }

  return response.json()
}

Axios и CSRF

Axios имеет встроенную поддержку XSRF.


Базовая конфигурация

import axios from 'axios'

export const api = axios.create({
  baseURL: '/api',
  withCredentials: true,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN'
})

Использование с TanStack Query

import { useMutation } from '@tanstack/react-query'
import { api } from './api'

export function useCreatePost() {
  return useMutation({
    mutationFn: async (data) => {
      const response = await api.post('/posts', data)
      return response.data
    }
  })
}

Axios автоматически:

  1. читает cookie;
  2. извлекает токен;
  3. отправляет его в header.

Double Submit Cookie

Принцип

Сервер:

  1. создаёт CSRF cookie;
  2. клиент отправляет тот же токен в header;
  3. сервер сравнивает значения.

Пример:

Cookie: XSRF-TOKEN=abc123

И одновременно:

X-XSRF-TOKEN: abc123

Злоумышленник не может прочитать cookie другого домена, поэтому подделка затрудняется.


SameSite Cookies

SameSite=Lax

Set-Cookie: session=abc123; SameSite=Lax

Cookie не отправляется в большинстве cross-site POST запросов.

Это базовый уровень защиты.


SameSite=Strict

Set-Cookie: session=abc123; SameSite=Strict

Наиболее строгий режим.

Cookie отправляется только при переходах внутри сайта.

Недостатки:

  • возможные проблемы с OAuth;
  • проблемы с SSO;
  • ограничения UX.

SameSite=None

Set-Cookie: session=abc123; SameSite=None; Secure

Разрешает cross-site cookies.

Наиболее опасный режим с точки зрения CSRF.

Требует обязательного использования дополнительных защит.


Проверка Origin и Referer

Проверка Origin

Сервер может проверять:

Origin: https://example.com

Если origin не совпадает — запрос отклоняется.


Проверка Referer

Referer: https://example.com/profile

Недостатки:

  • header может отсутствовать;
  • прокси могут его удалять;
  • браузеры могут скрывать данные.

Почему одного SameSite недостаточно

Некоторые разработчики ошибочно считают:

SameSite=Lax полностью решает CSRF

Это неверно.

Причины:

  • legacy browsers;
  • нестандартные сценарии;
  • iframe;
  • обходы через GET;
  • ошибки конфигурации;
  • сторонние интеграции.

CSRF token остаётся основным механизмом защиты.


Безопасная архитектура для TanStack Query

Рекомендуемая схема

Сервер

  • session cookie;
  • SameSite=Lax или Strict;
  • HttpOnly cookie;
  • Secure cookie;
  • CSRF token validation.

Клиент

  • единый API layer;
  • автоматическое добавление токена;
  • credentials: include;
  • централизованная обработка ошибок.

HttpOnly и CSRF

Распространённое заблуждение

Многие считают:

HttpOnly защищает от CSRF

Это неверно.

HttpOnly защищает только от XSS-кражи cookie.

Браузер всё равно автоматически отправляет HttpOnly cookie.

Следовательно CSRF остаётся возможным.


CSRF и JWT

JWT в cookies

Если JWT хранится в cookie:

Set-Cookie: access_token=...

CSRF всё ещё актуален.


JWT в localStorage

Если JWT хранится:

localStorage.setItem('token', jwt)

и передаётся вручную:

Authorization: Bearer token

то браузер автоматически не прикрепляет токен.

CSRF-атака становится значительно сложнее.

Но появляется повышенный риск XSS.


CSRF и React Server Components

В современных React-приложениях:

  • Next.js App Router;
  • RSC;
  • Server Actions;

CSRF снова становится особенно важным.

Поскольку многие server actions используют cookies и session auth.


Защита мутаций

Наиболее опасны:

useMutation()

Особенно:

  • удаление;
  • финансовые операции;
  • изменение email;
  • изменение пароля;
  • админские действия.

Дополнительные меры

Для критических операций применяют:

  • повторный ввод пароля;
  • confirmation dialog;
  • OTP;
  • re-authentication;
  • short-lived tokens.

Retry и CSRF

TanStack Query поддерживает retry.

Например:

retry: 3

Но при CSRF ошибке повтор запроса бессмысленен.


Правильная конфигурация

useMutation({
  mutationFn: updateUser,
  retry: (count, error) => {
    if (error.status === 403) {
      return false
    }

    return count < 3
  }
})

Обработка CSRF ошибок

Типичная ошибка

403 Forbidden

Либо:

{
  "message": "Invalid CSRF token"
}

Централизованный interceptor

async function api(url, options = {}) {
  const response = await fetch(url, options)

  if (response.status === 403) {
    const data = await response.json()

    if (data.code === 'CSRF_INVALID') {
      window.location.reload()
    }
  }

  return response.json()
}

Refresh CSRF Token

Некоторые приложения реализуют endpoint:

GET /csrf-token

Обновление токена

export async function refreshCsrfToken() {
  const response = await fetch('/csrf-token', {
    credentials: 'include'
  })

  const data = await response.json()

  setCsrfToken(data.token)
}

Интеграция с QueryClient

Иногда полезно автоматически обновлять CSRF token после login/logout.


Пример

const loginMutation = useMutation({
  mutationFn: login,
  onSuccess: async () => {
    await refreshCsrfToken()

    queryClient.invalidateQueries()
  }
})

SSR и CSRF

При SSR необходимо учитывать:

  • токен должен быть уникальным для сессии;
  • токен нельзя кэшировать;
  • нельзя шарить токены между пользователями.

Ошибочная практика

const csrfToken = 'static-token'

Статические токены полностью ломают защиту.


XSS и CSRF

XSS часто полностью обходит CSRF-защиту.

Если злоумышленник выполняет JS внутри приложения, он может:

  • читать CSRF token;
  • отправлять запросы;
  • вызывать API напрямую.

Поэтому безопасность требует:

  • CSP;
  • escaping;
  • sanitization;
  • защиты от inline scripts;
  • безопасной работы с HTML.

Практический production-подход

Наиболее распространённая схема

Backend

Session Cookie:
  HttpOnly
  Secure
  SameSite=Lax

CSRF

X-CSRF-Token header

Frontend

Centralized API layer

TanStack Query

All mutations go through API client

Пример production-конфигурации

api/client.js

let csrfToken = null

export function initializeCsrfToken(token) {
  csrfToken = token
}

export async function api(url, options = {}) {
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers
  }

  if (csrfToken) {
    headers['X-CSRF-Token'] = csrfToken
  }

  const response = await fetch(url, {
    ...options,
    credentials: 'include',
    headers
  })

  if (response.status === 403) {
    const error = await response.json()

    if (error.code === 'INVALID_CSRF') {
      throw new Error('CSRF_TOKEN_INVALID')
    }
  }

  if (!response.ok) {
    throw new Error('Request failed')
  }

  return response.json()
}

useDeleteAccount.js

import { useMutation } from '@tanstack/react-query'
import { api } from './api/client'

export function useDeleteAccount() {
  return useMutation({
    mutationFn: () =>
      api('/account', {
        method: 'DELETE'
      }),

    retry: false
  })
}

Типичные ошибки при защите от CSRF

Добавление токена только в некоторые запросы

Неправильно:

api.post('/user')
fetch('/admin/delete')

Второй запрос может оказаться без защиты.


Хранение CSRF token в глобальной переменной без обновления

Токен может устареть после:

  • refresh session;
  • relogin;
  • session rotation.

Отключение SameSite

Некоторые системы отключают:

SameSite=None

без необходимости.

Это значительно увеличивает риск CSRF.


Использование GET для изменения состояния

Опасно:

GET /delete-account

GET не должен изменять состояние.


Проверка безопасности API

При аудите необходимо проверять:

  • все ли mutation endpoints требуют CSRF;
  • защищены ли cookies;
  • есть ли SameSite;
  • используется ли Secure;
  • используется ли HttpOnly;
  • есть ли проверка Origin;
  • корректно ли обрабатываются 403 ошибки;
  • отсутствуют ли state-changing GET endpoints;
  • нет ли bypass через multipart/form-data;
  • нет ли исключений для mobile/webview.