Перейти к содержанию

Защита форм от CSRF

Сервер знает действующего пользователя и ограничивает каждую заметку её владельцем. Но браузер может приложить cookie к запросу, инициированному другой страницей. Если изменение требует только наличия этой cookie, сторонняя форма способна отправить действие от имени уже вошедшего человека.

Cross-site request forgery, или CSRF, связан с этой автоматической отправкой полномочия. Результат урока — проверка служебного токена для изменяющих запросов. Она дополняет право на заметку и не заменяет его.

Почему cookie недостаточно

Представим, что Alice открыла библиотеку, а затем другую страницу. Сессия остаётся в браузере. Приложение должно отличать форму своего интерфейса от произвольной попытки использовать эту сессию. Одного знания user_id для этого мало.

В нашей модели GET не меняет состояние. Изменение заметки и выход выполняются через POST. Такое разделение убирает случайное действие при открытии ссылки, но само по себе не защищает POST: сторонняя HTML-форма тоже умеет его отправлять.

SameSite Lax ограничивает часть межсайтовых отправок cookie. Однако site и origin являются разными понятиями, а продукт может позже изменить параметры. Поэтому явный токен сохраняется как проверяемый договор операции. Обзор механизмов доступен в OWASP CSRF Prevention.

Токен текущей сессии

При входе предыдущий урок уже создавал случайную строку csrf_token. Для формы входа нужна также анонимная сессия с токеном. Функция шаблона создаёт его при первом обращении и затем возвращает то же значение.

import secrets
from flask import session

def csrf_token():
    if "csrf_token" not in session:
        session["csrf_token"] = secrets.token_urlsafe(32)
    return session["csrf_token"]

Токен связан с подписанной сессией. Браузер может увидеть свой скрытый input, но сторонний origin не получает содержимое нашей страницы по обычному межсайтовому запросу. Непредсказуемость значения помогает отличить запрос, которому доступна форма.

Это не пароль и не одноразовый идентификатор операции. Один токен может сопровождать несколько форм текущей сессии. Защита от повторной оплаты или дублированного задания потребовала бы другого механизма; в нашей заметке нет таких операций.

Поле формы

В login.html, edit.html и форме выхода помещаем hidden input. Он не скрывается от самого пользователя, а передаёт значение обработчику.

<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

Функция регистрируется в контексте шаблонов Flask. Значение выводится стандартным способом. Не добавляйте токен в адресную строку: URL может оказаться в истории, журнале или ссылке. Он относится к телу изменяющего запроса.

После успешного входа session.clear и новое значение токена отделяют состояние до входа от новой личности. Если открытая ранее форма отправится со старым значением, она должна получить отказ и предложить заново открыть страницу.

Серверная проверка

Ниже содержательная проверка для before_request. Она выполняется до изменяющего маршрута. В JSON-API токен передаётся через X-CSRF-Token; обычная форма использует поле.

import hmac
from flask import request

if request.method in {"POST", "PUT", "PATCH", "DELETE"}:
    expected = session.get("csrf_token")
    if request.path.startswith("/api/"):
        supplied = request.headers.get("X-CSRF-Token")
    else:
        values = request.form.getlist("csrf_token")
        supplied = values[0] if len(values) == 1 else None
    if not isinstance(expected, str) or not isinstance(supplied, str):
        abort(403, description="Форма устарела")
    if not supplied.isascii() or len(supplied) != len(expected):
        abort(403, description="Форма устарела")
    if not hmac.compare_digest(expected, supplied):
        abort(403, description="Форма устарела")

Повторные значения поля отклоняются. Токен библиотеки состоит из ASCII-символов; другой алфавит и длина отклоняются до compare_digest, чтобы неверный Unicode-ввод не создавал внутреннюю ошибку. Не нужно возвращать ожидаемый токен в сообщении об ошибке: это разрушило бы смысл отказа и раскрытие сведений.

Проверка применяется также к форме входа. Подмена входа на чужую учётную запись может иметь собственные последствия, даже если у посетителя пока нет личной заметки. Для нашего общего middleware нет причины делать login свободным исключением.

Варианты наблюдения

Корректная форма Alice с текущим токеном может изменить её заметку. Та же форма без токена или с другой строкой должна получить 403 до записи. Вариант с правильным токеном Boris всё равно не получает заметку Alice: авторизация остаётся отдельным условием.

Порядок отказов может зависеть от общего обработчика. Например, запрос без действующей сессии не имеет ожидаемого токена и завершается раньше выбора объекта. Важно записывать исходное состояние, чтобы не принять один отказ за доказательство другой защиты.

Для проверки изменения сравните содержимое до и после ошибочного POST. Сам код 403 недостаточен, если действие уже выполнено. Поэтому проверка размещена перед функцией маршрута, а не после db.commit.

Ограничения механизма

Если XSS выполняет программу внутри нашей страницы, ей может быть доступен токен формы или возможность отправить корректный запрос. CSRF-защита не устраняет такую ошибку. Правильный вывод и политика ресурсов предыдущих уроков сохраняют своё назначение.

Токен не доказывает личность вне контекста сессии. Его нельзя использовать как отдельный API-ключ, отправлять другой системе или хранить в публичном файле. Для межсервисного доступа будет другой договор аутентификации.

Подтверждение Origin и другие браузерные сигналы могут усиливать защиту, но требуют политики отсутствующих заголовков и доверенного внешнего адреса. В маленьком стенде мы не выдаём их за реализованный универсальный фильтр.

Открытые вкладки и повтор

Две формы одной действующей сессии могут использовать один токен. Успех первой не делает вторую автоматически недействительной: наш механизм не является одноразовой операцией. Для заметки такой договор допустим, а защита от повторного задания или платежа потребовала бы собственного ID.

После нового входа токен меняется. Старая форма из другой вкладки получает 403 и должна быть открыта заново. Это полезный сценарий проверки: он показывает привязку к состоянию, а не только сравнение двух одинаковых строк в одном запросе.

Для API общий обработчик выбирает токен по пути /api/, а не по объявленному Content-Type. Тогда корректный header позволяет маршруту отдельно вернуть 415 для неподходящего типа тела. Если выбирать способ токена только через request.is_json, неправильный MIME скрывал бы эту другую проверку.

Заголовок X-CSRF-Token не делает программу доверенной вне текущей cookie. Нужны оба значения одного контекста. Передача чужого токена с собственной сессией должна завершиться отказом.

Отдельно проверьте GET выхода: в финальной модели такого изменяющего маршрута нет. Требование подлинности связано и с методом операции. Ссылка на выход не должна неожиданно выполнять действие при предварительном открытии браузером или роботом.

Теперь изменение требует корректной сессии, права на объект и токена формы. Следующий урок ограничит адрес, на который приложение возвращает посетителя после входа, чтобы удобная функция next не создала произвольное перенаправление.