Что такое CSRF-атака и как CSRF-токен помогает от неё защититься?

Ответ

Это отличный вопрос, который затрагивает и саму уязвимость, и способ защиты.

1. Сначала об атаке: что такое CSRF?

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

Пример:

  1. Вы залогинены в своем интернет-банке mybank.com.
  2. Вы открываете в новой вкладке вредоносный сайт evil.com.
  3. На этом сайте есть невидимая форма или картинка, которая автоматически отправляет запрос на mybank.com/transfer?to=attacker&amount=1000.
  4. Ваш браузер, имея валидные cookie для mybank.com, послушно отправит этот запрос вместе с ними. Банк посчитает запрос легитимным и выполнит перевод денег.

2. Как CSRF-токен решает эту проблему?

CSRF-токен — это уникальный, секретный и непредсказуемый код, который сервер генерирует для сессии пользователя и встраивает в веб-формы.

Механизм защиты:

  1. Когда пользователь открывает страницу с формой (например, для перевода денег), сервер генерирует токен и вставляет его в скрытое поле формы.
  2. Когда пользователь отправляет форму, этот токен уходит вместе с запросом.
  3. Сервер перед выполнением действия проверяет, совпадает ли токен из запроса с тем, что он сохранил для этой сессии.

Сайт злоумышленника (evil.com) не может узнать или угадать этот секретный токен, поэтому его поддельный запрос будет отклонен сервером из-за отсутствия или неверности токена.

Пример в Go с использованием gorilla/csrf:

package main

import (
    "html/template"
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/process", ProcessForm).Methods("POST")

    // 1. Создаем middleware для защиты
    // Ключ должен быть секретным и храниться в конфигурации
    csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))

    http.ListenAndServe(":8080", csrfMiddleware(r))
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
    // 2. Вставляем токен в шаблон с помощью csrf.TemplateField
    t, _ := template.New("signup.html").Parse(`
        <form action="/process" method="POST">
            <input type="text" name="name">
            {{ .CSRFField }}
            <button type="submit">Отправить</button>
        </form>
    `)
    t.Execute(w, map[string]interface{}{
        "CSRFField": csrf.TemplateField(r),
    })
}

func ProcessForm(w http.ResponseWriter, r *http.Request) {
    // 3. Если запрос дошел сюда, значит CSRF-токен валиден.
    // Middleware уже выполнил проверку.
    w.Write([]byte("Форма успешно обработана!"))
}

Ключевые характеристики хорошего CSRF-токена:

  • Уникальность: Должен быть уникальным для каждой сессии пользователя.
  • Непредсказуемость: Генерируется с помощью криптографически стойкого генератора случайных чисел.
  • Ограниченное время жизни: Токен должен устаревать вместе с сессией или через определенное время.

Ответ 18+ 🔞

А, ну это же классика, блядь! Просто пиздец, как часто эту хуйню спрашивают. Сейчас разжуем, как для дебила, но с технической точностью, ёпта.

Представь себе, сидишь ты в своём интернет-банке, типа mybank.com, авторизованный, как миленький. Куки твои, сессия — всё в ажуре. И тут ты, такой довольный, открываешь в другой вкладке сайт evil.com, где тебе показывают котиков или обещают увеличить член на 15 сантиметров за неделю.

А на этом сайте, сука, подлость! Там невидимая форма или даже просто картинка, которая, как только страница загрузилась, пиздует запрос на mybank.com/transfer?to=attacker&amount=1000. И браузер твой, тупая мартышлюшка, думая, что это ты сам захотел, отправляет этот запрос вместе со всеми твоими куками от банка. Банк смотрит: «А, это же наш Вася! Куки в порядке. Переводим тысячу на счёт злоумышленника». И ты остаёшься без бабла, даже не поняв, что произошло. Вот это и есть CSRF — межсайтовая подделка запроса. Тебя, по сути, подставили, используя твой же авторизованный браузер как оружие против тебя.

И как от этого спастись, блядь?

А вот тут на сцену выходит наш герой — CSRF-токен. Это такая хитрая, сука, заглушка, которую сервер вставляет в каждую форму, которую ты видишь.

Смысл в чём:

  1. Сервер, когда отдаёт тебе страницу с формой перевода, генерирует уникальный, криптостойкий, ебаный секретный код. И засовывает его в форму как скрытое поле.
  2. Ты жмёшь «отправить», и этот токен летит на сервер вместе с остальными данными.
  3. Сервер проверяет: «А токен-то тот ли, что я тебе дал?». Если совпал — всё ок, выполняем перевод. Если нет или его вообще нет — «иди нахуй, запрос поддельный».

В чём подвох для evil.com? Он не может этот токен ни украсть (из-за политики same-origin браузеров), ни угадать (токен криптостойкий). Поэтому его подлый запрос без токена будет послан нахуй банком. Защита, блядь!

Смотри, как это выглядит в коде на Go, если использовать популярную библиотеку gorilla/csrf:

package main

import (
    "html/template"
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/process", ProcessForm).Methods("POST")

    // 1. Вот эта строчка — наше всё. Создаём middleware, который будет нахуй посылать все запросы без токена.
    // Ключ должен быть длинным и секретным, а не как у некоторых — "12345".
    csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))

    http.ListenAndServe(":8080", csrfMiddleware(r))
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
    // 2. В шаблоне формы мы вставляем это волшебное поле. Оно само развернётся в `<input type="hidden" value="тут_длинный_токен">`.
    t, _ := template.New("signup.html").Parse(`
        <form action="/process" method="POST">
            <input type="text" name="name">
            {{ .CSRFField }}
            <button type="submit">Отправить</button>
        </form>
    `)
    t.Execute(w, map[string]interface{}{
        "CSRFField": csrf.TemplateField(r),
    })
}

func ProcessForm(w http.ResponseWriter, r *http.Request) {
    // 3. Если запрос дошёл сюда — поздравляю, токен прошёл проверку в middleware.
    // Можно спокойно обрабатывать. Если бы токен был невалидный, middleware вернул бы ошибку 403 Forbidden ещё до вызова этой функции.
    w.Write([]byte("Форма успешно обработана!"))
}

И главное, блядь, помни про токен:

  • Уникальный — для каждой сессии свой, а в идеале — для каждой формы.
  • Непредсказуемый — чтобы его нельзя было подобрать или угадать. Генератор случайных чисел должен быть серьёзный, а не rand.Intn(100).
  • Срок жизни — обычно привязан к сессии пользователя. Закрыл браузер — токен сдох. Или можно делать с таймаутом.

Вот и вся магия. Не такой уж и ёперный театр, правда? Главное — не забывать это хуйню использовать, а то потом удивляться, куда деньги с аккаунта деваются.