После входа в админку через 8 часов все изменения падают с «Неверный или отсутствующий CSRF-токен» #50

Closed
opened 2026-09-13 13:00:56 +03:00 by Agent · 2 comments
Collaborator

Что происходит

На проде (после выкладки v1.35) выбор любимой фракции отвечает «Неверный или отсутствующий CSRF-токен.». Проблема не только в фракции: в этом состоянии так же отказывает любое изменение данных (партии, «о себе», группы, аватар). Сайт при этом выглядит залогиненным, а повторная попытка не помогает. CSRF-код в релизе 1.35 не менялся, это старый баг.

Причина

Double-submit CSRF использует одну cookie csrf_token для игрока и для админки (backend/app/core/security.py):

  • set_user_session ставит fs_session и csrf_token со сроком JWT_USER_TTL_MINUTES (7 дней);
  • set_admin_session ставит fs_admin и перезаписывает csrf_token со сроком JWT_ADMIN_TTL_MINUTES (8 часов).

Через 8 часов после входа в админку браузер выбрасывает csrf_token, а fs_session живёт ещё до 7 дней. CSRFMiddleware (backend/app/main.py) видит сессионную cookie без CSRF-cookie и отвечает 403 CSRF_FAILED на каждую мутацию. Новый csrf_token сервер выдаёт только при следующем входе, поэтому само состояние не проходит.

Воспроизведение (TestClient, проверено)

  1. POST /api/auth/dev/login → csrf_token с Max-Age=604800.
  2. POST /api/admin/auth/login → csrf_token перезаписан с Max-Age=28800.
  3. Удалить csrf_token из jar (имитация истечения 8 часов).
  4. GET /api/users/me → 200 (сессия жива); PATCH /api/users/me/profile → 403 CSRF_FAILED.

В браузере: document.cookie на сайте не содержит csrf_token, хотя пользователь залогинен.

Обход до исправления: выйти из аккаунта и войти снова.

Направление решения

  • Сервер сам восстанавливает токен: если на запросе к /api есть действующая сессионная cookie (fs_session или fs_admin), а csrf_token нет, в ответ добавляется новый csrf_token. Фронт при загрузке страницы делает GET /api/users/me, так что следующая мутация уже пройдёт. Реализовывать на чистом ASGI, как CSRFMiddleware, без BaseHTTPMiddleware: он ломает SSE.
  • Вход в админку не должен сокращать срок токена игрока: срок csrf_token не короче срока самой долгой из действующих сессий.
  • Выход (clear_user_session / clear_admin_session) не должен ломать токен оставшейся сессии.

Критерии готовности

  • Тест-регрессия: сессия игрока без csrf_token → после любого безопасного запроса к /api мутация с токеном из cookie проходит (200), а не 403.
  • Тест: вход игрока, затем вход в админку — срок csrf_token не меньше срока fs_session.
  • Проверка CSRF на мутациях не ослаблена: запрос без заголовка или с чужим токеном по-прежнему получает 403.
  • SSE /api/events продолжает стримиться (без буферизации ответа).
# Что происходит На проде (после выкладки v1.35) выбор любимой фракции отвечает «Неверный или отсутствующий CSRF-токен.». Проблема не только в фракции: в этом состоянии так же отказывает **любое** изменение данных (партии, «о себе», группы, аватар). Сайт при этом выглядит залогиненным, а повторная попытка не помогает. CSRF-код в релизе 1.35 не менялся, это старый баг. # Причина Double-submit CSRF использует **одну** cookie `csrf_token` для игрока и для админки (`backend/app/core/security.py`): - `set_user_session` ставит `fs_session` и `csrf_token` со сроком `JWT_USER_TTL_MINUTES` (7 дней); - `set_admin_session` ставит `fs_admin` и **перезаписывает** `csrf_token` со сроком `JWT_ADMIN_TTL_MINUTES` (8 часов). Через 8 часов после входа в админку браузер выбрасывает `csrf_token`, а `fs_session` живёт ещё до 7 дней. `CSRFMiddleware` (`backend/app/main.py`) видит сессионную cookie без CSRF-cookie и отвечает 403 `CSRF_FAILED` на каждую мутацию. Новый `csrf_token` сервер выдаёт только при следующем входе, поэтому само состояние не проходит. # Воспроизведение (TestClient, проверено) 1. `POST /api/auth/dev/login` → `csrf_token` с `Max-Age=604800`. 2. `POST /api/admin/auth/login` → `csrf_token` перезаписан с `Max-Age=28800`. 3. Удалить `csrf_token` из jar (имитация истечения 8 часов). 4. `GET /api/users/me` → **200** (сессия жива); `PATCH /api/users/me/profile` → **403** `CSRF_FAILED`. В браузере: `document.cookie` на сайте не содержит `csrf_token`, хотя пользователь залогинен. **Обход до исправления:** выйти из аккаунта и войти снова. # Направление решения - Сервер сам восстанавливает токен: если на запросе к `/api` есть действующая сессионная cookie (`fs_session` или `fs_admin`), а `csrf_token` нет, в ответ добавляется новый `csrf_token`. Фронт при загрузке страницы делает `GET /api/users/me`, так что следующая мутация уже пройдёт. Реализовывать на чистом ASGI, как `CSRFMiddleware`, без `BaseHTTPMiddleware`: он ломает SSE. - Вход в админку не должен сокращать срок токена игрока: срок `csrf_token` не короче срока самой долгой из действующих сессий. - Выход (`clear_user_session` / `clear_admin_session`) не должен ломать токен оставшейся сессии. # Критерии готовности - Тест-регрессия: сессия игрока без `csrf_token` → после любого безопасного запроса к `/api` мутация с токеном из cookie проходит (200), а не 403. - Тест: вход игрока, затем вход в админку — срок `csrf_token` не меньше срока `fs_session`. - Проверка CSRF на мутациях не ослаблена: запрос без заголовка или с чужим токеном по-прежнему получает 403. - SSE `/api/events` продолжает стримиться (без буферизации ответа).
Agent added this to the 1.37 - смена аутентификации и смежный пен тест milestone 2026-09-13 13:00:56 +03:00
Agent added the Kind/BugKind/Security labels 2026-09-13 13:00:56 +03:00
Agent self-assigned this 2026-09-13 13:03:47 +03:00
Agent added the
Reviewed
Confirmed
1
label 2026-09-13 13:03:47 +03:00
Author
Collaborator

План выполнения

Уточнение к описанию: обход «выйти и войти» не работает. Выход — тоже POST, и его блокирует та же проверка. До исправления поможет только удаление cookie сайта в браузере.

  1. CSRFMiddleware (чистый ASGI): если запрос к /api несёт сессионную cookie, а csrf_token нет, в стартовое сообщение ответа дописывается Set-Cookie со свежим токеном. Это касается и 403-отказа. Тело ответа (SSE) идёт насквозь чанками.
  2. csrf_token при любом входе получает срок max(JWT_USER_TTL, JWT_ADMIN_TTL): вход в админку больше не сокращает токен игрока. Ротация токена при входе сохраняется.
  3. Проверка double-submit не ослабляется. Запрос с cookie, но без заголовка или с чужим токеном — по-прежнему 403, cookie при этом не перевыдаётся.
  4. Фронт и эндпойнты не меняются.
  5. Тесты backend/tests/test_csrf.py: восстановление токена на GET и на отказе, срок после входа админа, неизменная строгость проверки, анонимам токен не выдаётся, middleware не склеивает чанки потока.
  6. Отдельным коммитом — правка README.md владельца (деплой на Pi → scripts/build-push.ps1).

Критерии готовности

  • Сессия без csrf_token восстанавливается первым же запросом к /api; мутации и выход после этого проходят.
  • Вход в админку не делает срок csrf_token короче срока fs_session.
  • Мутация без заголовка или с неверным токеном — 403, как раньше.
  • /api/events стримится без буферизации; pytest зелёный.

Ветка: issue-50-csrf-token-refresh от dev

## План выполнения Уточнение к описанию: обход «выйти и войти» **не работает**. Выход — тоже POST, и его блокирует та же проверка. До исправления поможет только удаление cookie сайта в браузере. 1. `CSRFMiddleware` (чистый ASGI): если запрос к `/api` несёт сессионную cookie, а `csrf_token` нет, в стартовое сообщение ответа дописывается `Set-Cookie` со свежим токеном. Это касается и 403-отказа. Тело ответа (SSE) идёт насквозь чанками. 2. `csrf_token` при любом входе получает срок `max(JWT_USER_TTL, JWT_ADMIN_TTL)`: вход в админку больше не сокращает токен игрока. Ротация токена при входе сохраняется. 3. Проверка double-submit не ослабляется. Запрос с cookie, но без заголовка или с чужим токеном — по-прежнему 403, cookie при этом не перевыдаётся. 4. Фронт и эндпойнты не меняются. 5. Тесты `backend/tests/test_csrf.py`: восстановление токена на GET и на отказе, срок после входа админа, неизменная строгость проверки, анонимам токен не выдаётся, middleware не склеивает чанки потока. 6. Отдельным коммитом — правка `README.md` владельца (деплой на Pi → `scripts/build-push.ps1`). **Критерии готовности** - Сессия без `csrf_token` восстанавливается первым же запросом к `/api`; мутации и выход после этого проходят. - Вход в админку не делает срок `csrf_token` короче срока `fs_session`. - Мутация без заголовка или с неверным токеном — 403, как раньше. - `/api/events` стримится без буферизации; `pytest` зелёный. **Ветка:** `issue-50-csrf-token-refresh` от `dev`
Agent added the
Status
In Review
label 2026-09-13 13:14:52 +03:00
Author
Collaborator

Работа выполнена, открыт PR: #51

Итог: если сессия есть, а csrf_token нет, сервер перевыдаёт токен на любом ответе /api, в том числе на самом 403. Срок токена теперь не короче самой долгой сессии. Проверка мутаций не ослаблена, SSE не буферизуется. После выкладки залипшему пользователю достаточно перезагрузить страницу.
Проверки: test_csrf.py — 7 тестов, 4 из них падают на старом коде; вся сюита — 102 passed; живая проверка через uvicorn + curl: 403 с новым токеном → повтор 200, GET перевыдаёт токен → выход 200, /api/events стримится.
Статус: Status/In Review
Осталось за вами: ревью и мёрж PR — задача закроется автоматически. Для прода после мёржа: .\scripts\build-push.ps1 на ПК, затем docker compose up -d на Pi.

Работа выполнена, открыт PR: https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/pulls/51 **Итог:** если сессия есть, а `csrf_token` нет, сервер перевыдаёт токен на любом ответе `/api`, в том числе на самом 403. Срок токена теперь не короче самой долгой сессии. Проверка мутаций не ослаблена, SSE не буферизуется. После выкладки залипшему пользователю достаточно перезагрузить страницу. **Проверки:** `test_csrf.py` — 7 тестов, 4 из них падают на старом коде; вся сюита — 102 passed; живая проверка через uvicorn + `curl`: 403 с новым токеном → повтор 200, GET перевыдаёт токен → выход 200, `/api/events` стримится. **Статус:** `Status/In Review` **Осталось за вами:** ревью и мёрж PR — задача закроется автоматически. Для прода после мёржа: `.\scripts\build-push.ps1` на ПК, затем `docker compose up -d` на Pi.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: NotBigGhost/ForbiddenStarsApp#50