CSRF
CSRF — запрос, который чужой сайт пытается выполнить от имени пользователя, используя автоматически отправляемую браузером cookie. В Cellestial браузерные RPC принимает BFF: он проверяет заголовок X-CSRF: 1, а для POST — также источник запроса. Ни CSRF-токен из cookie, ни его получение отдельным запросом клиенту не нужны.
Cookie сессии
Сессия находится в cookie __Host-Http-session с атрибутами Path=/; HttpOnly; Secure; SameSite=Lax, без Domain. BFF шифрует её содержимое AES-256-GCM; внутри находятся токены Go и сроки их действия. JavaScript страницы не получает открытые токены и не читает cookie.
HttpOnly ограничивает чтение cookie, а Secure — её отправку по незащищённому соединению. SameSite=Lax ограничивает отправку при обращении с другого сайта, но не заменяет проверку источника: поддомены могут относиться к одному site, оставаясь разными origin. Клиент вызывает /api/trpc через единый транспорт с credentials: 'same-origin'.
Проверки BFF
Проверки выполняются до чтения сессии и вызовов Go, в том числе для входа и регистрации. Каждый RPC требует точного значения X-CSRF: 1. Для POST Origin обязан совпадать с APP_ORIGIN; пустое значение, null и другой origin отклоняются. Sec-Fetch-Site может отсутствовать; если он есть у POST, допускается только same-origin. Значения same-site, cross-site и none не проходят.
Любой отказ этих проверок — HTTP 403, tRPC FORBIDDEN и appCode: csrf_invalid. Успешная проверка CSRF ещё не означает успешную авторизацию: защищённая процедура без сессии вернёт 401, а неверные входные данные могут дать 400.
Для GET действует обязательная проверка X-CSRF. Проверки Origin и Sec-Fetch-Site в этом контексте применяются только к POST.
%% Flowchart: обязательный заголовок, дополнительные проверки POST и общий отказ до Go.
flowchart TD
rpc["RPC в BFF"] --> header{"X-CSRF равен 1?"}
header -->|Нет| reject["403 csrf_invalid: Go не вызывается"]
header -->|Да| method{"POST?"}
method -->|Нет| session["Чтение сессии и обработка процедуры"]
method -->|Да| origin{"Origin равен APP_ORIGIN?"}
origin -->|Нет| reject
origin -->|Да| site{"Sec-Fetch-Site отсутствует или same-origin?"}
site -->|Нет| reject
site -->|Да| session
classDef error fill:#fecaca,stroke:#b91c1c,color:#374151
classDef success fill:#a7f3d0,stroke:#047857,color:#374151
class reject error
class session successЗагрузка схемы…
Почему фиксированного заголовка достаточно для этой проверки
Значение 1 не является секретом. Защита опирается на ограничения браузера и проверку источника: обычная HTML-форма чужого сайта не умеет добавить произвольный X-CSRF, а cross-origin fetch с таким заголовком требует разрешающего CORS preflight. BFF не открывает CORS-доступ чужим origin. Для POST дополнительно проверяется сам Origin. Клиентский JavaScript не может произвольно подставить браузерные Origin и Sec-Fetch-Site.
Эти проверки защищают от запросов чужого сайта. Скрипт, уже исполняющийся внутри нашего origin, может отправить правильный заголовок и запрос с cookie. Поэтому защита от XSS нужна независимо от CSRF.
Граница BFF и Go
Публичные запросы приложения поступают через Caddy в BFF. BFF принимает RPC на /api/trpc; неизвестные /api/* не проксирует в Go. Браузер не вызывает Go напрямую.
Для серверного вызова BFF добавляет X-BFF-Key. Go проверяет этот ключ на API-маршрутах; для защищённых операций использует Authorization: Bearer и проверяет сессию. Browser cookie не служит способом авторизации Go. Проверка сервисного ключа не заменяет браузерную CSRF-защиту: она относится к другому участку запроса.
Источники и проверка
Исходники frontend — 007d8d4de7fa964947ffe1316c31726bbd386903; этот SHA указан в meta[name=release] HTML, полученного на проде 04.11.2026 около 01:17 МСК.
- Контекст BFF: порядок и условия проверок.
- Атрибуты cookie и шифрование сессии.
- Транспорт клиента и маршруты BFF.
- Публичный маршрут в Caddy и проверки S2S/Bearer в Go.
В Chrome через Obscura на указанную дату гостевые GET users.me и POST auth.login без заголовка дали 403 csrf_invalid. С заголовком GET дошёл до 401, а POST с пустым JSON — до 400 с ошибками полей. Старый GET /api/v1/users/me вернул 404. Локально прошли шесть тестов отказов CSRF, включая чужой Origin и same-site.
Матрица запросов с чужого origin и авторизованные сценарии на проде этим прогоном не проверены. SHA backend и infra выше подтверждают прочитанный код, а не версию запущенного процесса; серверный участок BFF → Go в этом прогоне не проверялся.