Пример обучающего модуля

Как мы превращаем реальный инцидент в практическое обучение для команды

🎯 Реальный инцидент 💻 Java + Spring ⚠️ CSRF Attack ⏱️ 15 минут обучения

🚨 Что произошло в вашей компании

15 января 2025: Клиент банка сообщил, что с его счёта произошёл несанкционированный перевод на сумму 50 000 ₽ на неизвестный счёт. Расследование показало, что перевод был совершён через легитимную сессию клиента в интернет-банке russianbank.ru, но он утверждает, что не совершал этого действия.

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

📚 Теория: Что такое CSRF и почему это работает

🔍 Что такое CSRF (Cross-Site Request Forgery)?

CSRF — это атака, при которой злоумышленник заставляет браузер жертвы отправить запрос на целевой сайт без её ведома. Главная особенность: браузер автоматически добавляет cookie аутентификации к каждому запросу на домен, для которого они были установлены.

🧩 Почему браузеры так работают?

Это фундаментальное поведение веб-браузеров, заложенное в спецификации HTTP:

  • Автоматическая отправка cookie: Когда браузер делает запрос к russianbank.ru, он автоматически прикрепляет все cookie для этого домена. Браузер не проверяет, откуда пришёл запрос (с russianbank.ru или с evil.com).
  • Same-Origin Policy (SOP): SOP защищает от чтения ответа с другого домена, но не блокирует отправку запроса. Поэтому злоумышленник может отправить POST-запрос, но не сможет прочитать ответ.
  • Исторические причины: Это поведение было необходимо для работы форм, iframe, изображений и других элементов, которые загружаются с разных доменов.

⚙️ Как работает механика атаки?

1. Условие для атаки:

  • Жертва авторизована на целевом сайте (есть валидная сессия в cookie)
  • Целевой сайт не использует CSRF-защиту
  • Злоумышленник знает структуру запроса (URL, параметры)

2. Что делает злоумышленник:

  • Создаёт вредоносную HTML-страницу с формой или JavaScript
  • Заманивает жертву на свою страницу (фишинг, реклама, письмо)
  • Форма автоматически отправляет запрос на целевой сайт
  • Браузер добавляет cookie → сервер думает, что это легитимный запрос

🎯 Типы CSRF атак

1. GET-based CSRF (простейший):


<img src="https://russianbank.ru/api/transfer?to=attacker&amount=50000"/>

2. POST-based CSRF (наш кейс):

<form action="https://russianbank.ru/api/transfer" method="POST">
  <input name="toAccount" value="attacker-account"/>
</form>
<script>document.forms[0].submit();</script>

3. JSON-based CSRF (современные API):

<script>
fetch('https://russianbank.ru/api/transfer', {
  method: 'POST',
  credentials: 'include', // отправить cookie
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ toAccount: 'attacker', amount: 50000 })
});
</script>

⚠️ Этот вариант частично блокируется CORS, но может работать если сервер неправильно настроен.

🛡️ Почему традиционные защиты НЕ работают?

❌ "У нас HTTPS" — НЕ ЗАЩИЩАЕТ

HTTPS шифрует трафик, но не проверяет легитимность источника запроса. CSRF работает поверх HTTPS.

❌ "У нас HttpOnly cookie" — НЕ ЗАЩИЩАЕТ

HttpOnly защищает от XSS (JavaScript не может прочитать cookie), но браузер всё равно автоматически отправляет эти cookie с CSRF-запросом.

❌ "Мы проверяем Referer" — НЕНАДЁЖНО

Некоторые браузеры и корпоративные прокси удаляют Referer. Это не должно быть единственной защитой.

❌ "У нас только POST, не GET" — НЕ ЗАЩИЩАЕТ

Злоумышленник может отправить POST-запрос через скрытую форму (как в нашем кейсе).

✅ Правильные методы защиты

1. CSRF Token (Synchronizer Token Pattern)

Сервер генерирует уникальный токен для каждой сессии/запроса. Токен передаётся в форме (не в cookie!). Сервер проверяет совпадение токена из cookie и из тела запроса.

2. SameSite Cookie Attribute

Set-Cookie: JSESSIONID=xxx; SameSite=Strict — браузер не отправит cookie при запросе с другого домена.

3. Double Submit Cookie Pattern

CSRF-токен хранится и в cookie, и в заголовке/параметре запроса. Злоумышленник не может прочитать cookie (SOP), значит не может подделать токен.

4. Custom Headers для AJAX

Для API: требовать кастомный заголовок (например, X-Requested-With: XMLHttpRequest). CORS не позволит простому HTML-форме добавить кастомный заголовок.

5. Проверка Origin/Referer (дополнительно)

Как дополнительный слой защиты: проверять, что Origin или Referer совпадает с доменом сервера.

📊 Реальная статистика и impact

  • OWASP Top 10: CSRF входит в категорию "Broken Access Control" (A01:2021)
  • CVSS Score: Обычно 6.5-8.1 (High) для финансовых систем
  • Bug Bounty: Средняя выплата $500-$5000 за CSRF в критичных функциях (HackerOne, 2024)
  • Известные инциденты: Netflix (2006), Gmail (2007), ING Direct (2008), YouTube (2008), McAfee (2010)

🔗 Связанные уязвимости

CSRF часто комбинируется с другими атаками:

  • XSS + CSRF: XSS позволяет прочитать CSRF-токен и обойти защиту
  • Clickjacking + CSRF: Пользователь думает, что кликает на кнопку "Скачать", а на самом деле подтверждает транзакцию
  • Session Fixation + CSRF: Злоумышленник фиксирует сессию жертвы, затем использует CSRF
  • CORS Misconfiguration + CSRF: Неправильный CORS позволяет читать ответы с другого домена

💡 Ключевой вывод:

CSRF работает не из-за бага в браузере, а из-за фундаментального дизайна HTTP. Единственная надёжная защита — явная проверка на сервере, что запрос пришёл из легитимного источника (через CSRF token или SameSite cookie).

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

1

Как именно вас взломали (пошагово)

Детальная визуализация атаки — как это выглядело изнутри

Шаг 1: Клиент авторизован в интернет-банке

Клиент залогинен в интернет-банке russianbank.ru. В браузере есть активная сессия с cookie JSESSIONID=7f8a9b2c1d.

Legitimate Session
// Cookie в браузере клиента
Cookie: JSESSIONID=7f8a9b2c1d; Domain=russianbank.ru; HttpOnly; Secure

Шаг 2: Клиент открывает вредоносную страницу

Злоумышленник отправляет клиенту email: "Вы выиграли iPhone 17! Кликните здесь". Клиент переходит на сайт злоумышленника free-iphone-russia.com.

Шаг 3: Скрытая форма отправляет запрос

На странице free-iphone-russia.com находится невидимая HTML форма, которая автоматически отправляется при загрузке страницы:

Malicious Page

<html>
  <body>
    <h1>Поздравляем! Вы выиграли iPhone 17!</h1>
    
    <!-- Скрытая форма, автоматически отправляющаяся -->
    <form id="attack" action="https://russianbank.ru/api/transfer" method="POST">
      <input type="hidden" name="toAccount" value="40817810099910004312"/>
      <input type="hidden" name="amount" value="50000"/>
    </form>
    
    <script>
      // Автоматически отправляем форму
      document.getElementById('attack').submit();
    </script>
  </body>
</html>

Шаг 4: Браузер автоматически добавляет cookie

Браузер видит, что запрос идёт на russianbank.ru и автоматически прикрепляет cookie сессии JSESSIONID=7f8a9b2c1d. Это стандартное поведение браузера — он не знает, что запрос инициирован злоумышленником.

Malicious Request
POST /api/transfer HTTP/1.1
Host: russianbank.ru
Cookie: JSESSIONID=7f8a9b2c1d  ← Браузер добавил автоматически!
Content-Type: application/x-www-form-urlencoded
Origin: https://free-iphone-russia.com

toAccount=40817810099910004312&amount=50000

Шаг 5: Сервер выполняет перевод

Backend russianbank.ru видит валидную сессию (JSESSIONID=7f8a9b2c1d) и выполняет перевод. Сервер думает, что это легитимный запрос от клиента.

Vulnerable Backend
// Уязвимый код — НЕТ CSRF защиты
@PostMapping("/api/transfer")
public ResponseEntity<String> transfer(
    @RequestParam String toAccount,
    @RequestParam BigDecimal amount,
    HttpSession session) {
    
    // Проверяем только сессию (этого недостаточно!)
    User user = (User) session.getAttribute("user");
    if (user == null) {
        return ResponseEntity.status(401).body("Unauthorized");
    }
    
    // ⚠️ ПРОБЛЕМА: Нет проверки CSRF токена!
    // Запрос может прийти откуда угодно
    
    bankService.transfer(user.getAccountId(), toAccount, amount);
    return ResponseEntity.ok("Transfer successful");
}

Шаг 6: Деньги ушли

✅ Перевод выполнен успешно. 50 000 ₽ ушли со счёта клиента на счёт злоумышленника. Клиент даже не видел, что произошло — всё случилось в фоне при загрузке страницы free-iphone-russia.com.

Визуальная схема атаки

👤 Клиент авторизован на russianbank.ru
🎣 Злоумышленник отправляет фишинговую ссылку на "Выиграй iPhone 17"
👤 Клиент открывает free-iphone-russia.com
📤 Скрытая форма отправляет POST на russianbank.ru/api/transfer
🍪 Браузер автоматически добавляет cookie сессии russianbank.ru
🏦 Backend russianbank.ru выполняет перевод (сессия валидна!)
💰 50 000 ₽ ушли на счёт злоумышленника
2

Почему защита не сработала

Разбор уязвимого кода и архитектурных ошибок

❌ Что было НЕ ТАК

  • ✗ Проверялась только наличие сессии
  • ✗ Не было CSRF токена
  • ✗ Не было проверки Origin/Referer заголовков
  • ✗ Spring Security CSRF защита была отключена
  • ✗ Критичные операции не требовали дополнительного подтверждения

✓ Что ДОЛЖНО было быть

  • ✓ CSRF токен в каждом state-changing запросе
  • ✓ Валидация токена на backend
  • ✓ SameSite cookie атрибут
  • ✓ Проверка Origin заголовка
  • ✓ Критичные операции с подтверждением (2FA/SMS)
Проблема
// ❌ CSRF защита была явно отключена в конфигурации
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()  ← ⚠️ КРИТИЧЕСКАЯ ОШИБКА!
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        return http.build();
    }
}
3

Как нужно писать правильно в вашем стеке

Spring Boot + Spring Security — правильное решение

✅ Правильное решение

Включаем встроенную CSRF защиту Spring Security и правильно настраиваем frontend для отправки токенов.

Backend: Включаем CSRF защиту

✓ Correct
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // ✅ CSRF защита включена по умолчанию в Spring Security
            // Но мы явно настраиваем для ясности
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        return http.build();
    }
}

Frontend: Отправляем CSRF токен

✓ Correct
// JavaScript на frontend
function getCsrfToken() {
    // Spring Security кладёт токен в cookie XSRF-TOKEN
    const name = "XSRF-TOKEN";
    const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
    return match ? match[2] : null;
}

// При отправке формы добавляем токен в заголовок
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-XSRF-TOKEN': getCsrfToken()  ← ✅ Добавляем токен!
    },
    body: JSON.stringify({
        toAccount: 'recipient-123',
        amount: 50000
    })
});

Дополнительная защита: SameSite Cookie

✓ Defense in Depth
@Configuration
public class CookieConfig {
    
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setSameSite("Strict");  ← ✅ SameSite=Strict
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(true);
        return serializer;
    }
}
4

Паттерн для вашей команды

Что теперь обязательно всегда

📋 Обязательные правила для всех state-changing операций

  • Всегда включена CSRF защита Spring Security
    Никогда не отключать .csrf().disable() без явного обоснования и approval от security team
  • Все POST/PUT/DELETE/PATCH запросы требуют CSRF токен
    Frontend обязан отправлять X-XSRF-TOKEN заголовок
  • SameSite=Strict для session cookies
    Браузер не будет отправлять cookie при cross-site запросах
  • Критичные операции (переводы > 10,000 ₽) требуют 2FA
    SMS/Email подтверждение для high-risk операций
  • Code review чеклист: проверяем CSRF защиту
    В каждом PR с новым endpoint проверяем наличие CSRF токена
5

Регресс-тест в CI/CD

Стоп-кран, который не даст этой уязвимости вернуться

🔒 Автоматический тест блокирует релиз

Если кто-то случайно отключит CSRF защиту или забудет проверить токен — этот тест упадёт и заблокирует деплой в production.

Regression Test
@SpringBootTest
@AutoConfigureMockMvc
class CsrfProtectionTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void transferWithoutCsrfToken_shouldBeForbidden() throws Exception {
        // Arrange: создаём authenticated сессию
        MvcResult loginResult = mockMvc.perform(post("/login")
                .param("username", "testuser")
                .param("password", "password"))
            .andExpect(status().isOk())
            .andReturn();
        
        String sessionCookie = loginResult.getResponse().getCookie("JSESSIONID").getValue();
        
        // Act: пытаемся сделать перевод БЕЗ CSRF токена
        // (имитируем CSRF атаку)
        mockMvc.perform(post("/api/transfer")
                .cookie(new Cookie("JSESSIONID", sessionCookie))
                .param("toAccount", "attacker-123")
                .param("amount", "50000"))
            // Assert: ожидаем 403 Forbidden
            .andExpect(status().isForbidden());  ← ✅ Запрос должен быть отклонён!
    }
    
    @Test
    void transferWithValidCsrfToken_shouldSucceed() throws Exception {
        // Arrange: получаем CSRF токен
        MvcResult result = mockMvc.perform(get("/api/csrf"))
            .andExpect(status().isOk())
            .andReturn();
        
        String csrfToken = result.getResponse().getCookie("XSRF-TOKEN").getValue();
        
        // Act: делаем перевод С валидным CSRF токеном
        mockMvc.perform(post("/api/transfer")
                .header("X-XSRF-TOKEN", csrfToken)
                .param("toAccount", "recipient-123")
                .param("amount", "1000"))
            // Assert: ожидаем успех
            .andExpect(status().isOk());  ← ✅ С токеном запрос проходит
    }
}

✅ Этот тест запускается в CI/CD

Если тест падает — релиз блокируется. Никто не сможет задеплоить код без CSRF защиты.

Интеграция: GitHub Actions / GitLab CI / Jenkins

Это один модуль Bug2Lab

Мы превращаем каждый ваш реальный инцидент в такое же практическое обучение + стоп-кран в CI/CD за 2-5 дней.

Забронировать 20-мин Discovery

Покажем демо и дадим расчёт под ваш стек