CSRF (Cross-Site Request Forgery)

CSRF — атака, при которой злоумышленник заставляет браузер жертвы отправить запрос к доверенному сайту, используя авторизованную сессию жертвы.

Зачем нужно

CSRF позволяет выполнять действия от имени пользователя без его ведома: перевести деньги, сменить пароль, удалить аккаунт. Браузер автоматически прикладывает cookies к запросу — атакующий этим пользуется.

Где используется

Любое веб-приложение с аутентификацией на основе cookies: банкинг, e-commerce, CMS, соцсети, панели администрирования.

Предпосылки

HTTP-методы, cookies, HTML-формы, XSS

Как работает CSRF

1. Жертва авторизована на bank.com (cookie сессии в браузере)

2. Жертва заходит на evil.com (фишинговая ссылка, реклама)

3. evil.com содержит:
   <form action="https://bank.com/transfer" method="POST">
     <input type="hidden" name="to" value="attacker">
     <input type="hidden" name="amount" value="10000">
   </form>
   <script>document.forms[0].submit();</script>

4. Браузер отправляет POST на bank.com С cookies жертвы

5. bank.com видит валидную сессию → выполняет перевод

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

// УЯЗВИМЫЙ КОД — нет защиты от CSRF
app.post('/transfer', (req, res) => {
  const { to, amount } = req.body;
  const user = req.session.user; // Cookie автоматически приложен

  db.transfer(user.id, to, amount); // Выполняет перевод!
  res.json({ success: true });
});

Варианты CSRF-атак

<!-- 1. Скрытая форма (POST) -->
<form action="https://bank.com/transfer" method="POST" id="csrfForm">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrfForm').submit();</script>

<!-- 2. Через img (GET-запрос) -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">

<!-- 3. Через iframe -->
<iframe src="https://bank.com/delete-account" style="display:none"></iframe>

Защита от CSRF

1. CSRF-токен (Synchronizer Token)

Сервер генерирует уникальный токен и вставляет в форму. При отправке проверяет.

// Сервер — генерация и проверка CSRF-токена
const crypto = require('crypto');
const csrf = require('csurf');

// Middleware
const csrfProtection = csrf({ cookie: true });

app.get('/transfer', csrfProtection, (req, res) => {
  // Вставляем токен в форму
  res.render('transfer', { csrfToken: req.csrfToken });
});

app.post('/transfer', csrfProtection, (req, res) => {
  // csurf автоматически проверяет _csrf в body/query/header
  const { to, amount } = req.body;
  db.transfer(req.session.user.id, to, amount);
  res.json({ success: true });
});
<!-- HTML-форма с CSRF-токеном -->
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <input name="to" placeholder="Кому">
  <input name="amount" placeholder="Сумма">
  <button type="submit">Перевести</button>
</form>
// Для SPA: токен в заголовке
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken, // Получен из meta-тега или cookie
  },
  body: JSON.stringify({ to: 'bob', amount: 100 }),
});

2. SameSite Cookies

// Express — установка SameSite
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict', // Cookie НЕ отправляется с кросс-сайтовых запросов
});
SameSite Поведение Использование
Strict Cookie не отправляется при переходе с другого сайта Максимальная защита
Lax Cookie отправляется только при навигации (GET) Баланс UX и безопасности
None Cookie отправляется всегда (нужен Secure) Для кросс-сайтовых сценариев
// Lax — хорошо для большинства случаев
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax', // GET-навигация ОК, POST с другого сайта — нет
});

Токен хранится в cookie И отправляется в заголовке/body. Атакующий не может прочитать cookie другого домена.

// Сервер — установка CSRF-cookie
app.use((req, res, next) => {
  if (!req.cookies.csrf) {
    const token = crypto.randomBytes(32).toString('hex');
    res.cookie('csrf', token, {
      httpOnly: false, // JS должен прочитать!
      secure: true,
      sameSite: 'Lax',
    });
  }
  next;
});

// Проверка
app.post('/api/*', (req, res, next) => {
  const cookieToken = req.cookies.csrf;
  const headerToken = req.headers['x-csrf-token'];

  if (!cookieToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'CSRF token invalid' });
  }
  next;
});
// Клиент — читает cookie и кладёт в заголовок
function getCookie(name) {
  const match = document.cookie.match(new RegExp(`${name}=([^;]+)`));
  return match ? match[1] : null;
}

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': getCookie('csrf'),
  },
  body: JSON.stringify(data),
});

4. Проверка заголовков Origin / Referer

app.use((req, res, next) => {
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const origin = req.headers.origin || req.headers.referer;
    const allowed = 'https://myapp.com';

    if (!origin || !origin.startsWith(allowed)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
  }
  next;
});

Чек-лист защиты

Мера Надёжность Сложность
SameSite=Strict/Lax cookies Высокая Низкая
CSRF-токен Высокая Средняя
Double Submit Cookie Высокая Средняя
Проверка Origin/Referer Средняя Низкая
Не использовать GET для изменений Базовая Низкая

Частые ошибки

1. Изменения через GET-запросы

// ПЛОХО: GET меняет данные
app.get('/delete-account', (req, res) => {
  db.deleteUser(req.session.userId);
});
// Атака: <img src="https://app.com/delete-account">

// ХОРОШО: используй POST/DELETE
app.delete('/account', csrfProtection, (req, res) => {
  db.deleteUser(req.session.userId);
});

2. SameSite=None без причины

// ПЛОХО: отключает защиту
res.cookie('session', token, { sameSite: 'None', secure: true });

// ХОРОШО: Lax или Strict
res.cookie('session', token, { sameSite: 'Lax', secure: true });

3. Предсказуемые CSRF-токены

// ПЛОХО: токен на основе timestamp
const token = Date.now().toString();

// ХОРОШО: криптографически случайный
const token = crypto.randomBytes(32).toString('hex');

Практика

  1. Создай Express-приложение с формой перевода и реализуй CSRF-токен через csurf
  2. Настрой SameSite=Lax для session cookie
  3. Реализуй Double Submit Cookie паттерн вручную (без библиотек)
  4. Создай тестовую HTML-страницу, имитирующую CSRF-атаку, и убедись, что защита работает

Связанные темы

Ресурсы