CSRF (Cross-Site Request Forgery) — атака, при которой злоумышленник заставляет браузер пользователя выполнить запрос к доверенному сайту от имени уже авторизованного пользователя.
Главная особенность CSRF заключается в том, что браузер автоматически прикрепляет к запросу:
Если сервер не проверяет источник запроса и не использует защитные механизмы, злоумышленник может инициировать выполнение критических операций:
Пример уязвимого запроса:
POST /api/user/email HTTP/1.1
Cookie: session=abc123
Content-Type: application/json
{
"email": "attacker@example.com"
}
Если пользователь авторизован, браузер автоматически отправит cookie
session=abc123, даже если запрос инициирован сторонним
сайтом.
TanStack Query активно используется для:
useMutation);В большинстве приложений TanStack Query работает совместно с:
fetch;axios;Именно cookie-based авторизация наиболее подвержена CSRF-атакам.
Если приложение использует:
Set-Cookie: session=...
то браузер будет автоматически прикреплять cookie ко всем запросам к домену.
Это создаёт потенциальную поверхность атаки.
На практике защита строится на комбинации:
Сервер генерирует уникальный CSRF token и передаёт его клиенту.
Клиент обязан отправлять токен в каждом изменяющем запросе:
Сервер сверяет токен с ожидаемым значением.
Если токен отсутствует или неверен — запрос отклоняется.
Часто сервер возвращает токен:
{
"csrfToken": "a1b2c3d4"
}
Либо:
<meta name="csrf-token" content="a1b2c3d4">
Либо через cookie:
Set-Cookie: XSRF-TOKEN=a1b2c3
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()
}
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
}
Такой подход приводит к:
Гораздо безопаснее использовать единый API layer.
src/
api/
client.js
csrf.js
auth.js
let csrfToken = null
export function setCsrfToken(token) {
csrfToken = token
}
export function getCsrfToken() {
return csrfToken
}
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.
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 имеет встроенную поддержку XSRF.
import axios from 'axios'
export const api = axios.create({
baseURL: '/api',
withCredentials: true,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN'
})
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 автоматически:
Сервер:
Пример:
Cookie: XSRF-TOKEN=abc123
И одновременно:
X-XSRF-TOKEN: abc123
Злоумышленник не может прочитать cookie другого домена, поэтому подделка затрудняется.
Set-Cookie: session=abc123; SameSite=Lax
Cookie не отправляется в большинстве cross-site POST запросов.
Это базовый уровень защиты.
Set-Cookie: session=abc123; SameSite=Strict
Наиболее строгий режим.
Cookie отправляется только при переходах внутри сайта.
Недостатки:
Set-Cookie: session=abc123; SameSite=None; Secure
Разрешает cross-site cookies.
Наиболее опасный режим с точки зрения CSRF.
Требует обязательного использования дополнительных защит.
Сервер может проверять:
Origin: https://example.com
Если origin не совпадает — запрос отклоняется.
Referer: https://example.com/profile
Недостатки:
Некоторые разработчики ошибочно считают:
SameSite=Lax полностью решает CSRF
Это неверно.
Причины:
CSRF token остаётся основным механизмом защиты.
Многие считают:
HttpOnly защищает от CSRF
Это неверно.
HttpOnly защищает только от XSS-кражи cookie.
Браузер всё равно автоматически отправляет HttpOnly cookie.
Следовательно CSRF остаётся возможным.
Если JWT хранится в cookie:
Set-Cookie: access_token=...
CSRF всё ещё актуален.
Если JWT хранится:
localStorage.setItem('token', jwt)
и передаётся вручную:
Authorization: Bearer token
то браузер автоматически не прикрепляет токен.
CSRF-атака становится значительно сложнее.
Но появляется повышенный риск XSS.
В современных React-приложениях:
CSRF снова становится особенно важным.
Поскольку многие server actions используют cookies и session auth.
Наиболее опасны:
useMutation()
Особенно:
Для критических операций применяют:
TanStack Query поддерживает retry.
Например:
retry: 3
Но при CSRF ошибке повтор запроса бессмысленен.
useMutation({
mutationFn: updateUser,
retry: (count, error) => {
if (error.status === 403) {
return false
}
return count < 3
}
})
403 Forbidden
Либо:
{
"message": "Invalid CSRF token"
}
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()
}
Некоторые приложения реализуют endpoint:
GET /csrf-token
export async function refreshCsrfToken() {
const response = await fetch('/csrf-token', {
credentials: 'include'
})
const data = await response.json()
setCsrfToken(data.token)
}
Иногда полезно автоматически обновлять CSRF token после login/logout.
const loginMutation = useMutation({
mutationFn: login,
onSuccess: async () => {
await refreshCsrfToken()
queryClient.invalidateQueries()
}
})
При SSR необходимо учитывать:
const csrfToken = 'static-token'
Статические токены полностью ломают защиту.
XSS часто полностью обходит CSRF-защиту.
Если злоумышленник выполняет JS внутри приложения, он может:
Поэтому безопасность требует:
Session Cookie:
HttpOnly
Secure
SameSite=Lax
X-CSRF-Token header
Centralized API layer
All mutations go through API client
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()
}
import { useMutation } from '@tanstack/react-query'
import { api } from './api/client'
export function useDeleteAccount() {
return useMutation({
mutationFn: () =>
api('/account', {
method: 'DELETE'
}),
retry: false
})
}
Неправильно:
api.post('/user')
fetch('/admin/delete')
Второй запрос может оказаться без защиты.
Токен может устареть после:
Некоторые системы отключают:
SameSite=None
без необходимости.
Это значительно увеличивает риск CSRF.
Опасно:
GET /delete-account
GET не должен изменять состояние.
При аудите необходимо проверять: