На проде (после выкладки 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, проверено)
POST /api/auth/dev/login → csrf_token с Max-Age=604800.
POST /api/admin/auth/login → csrf_token перезаписан с Max-Age=28800.
Удалить csrf_token из jar (имитация истечения 8 часов).
GET /api/users/me → 200 (сессия жива); PATCH /api/users/me/profile → 403CSRF_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
Уточнение к описанию: обход «выйти и войти» не работает. Выход — тоже POST, и его блокирует та же проверка. До исправления поможет только удаление cookie сайта в браузере.
CSRFMiddleware (чистый ASGI): если запрос к /api несёт сессионную cookie, а csrf_token нет, в стартовое сообщение ответа дописывается Set-Cookie со свежим токеном. Это касается и 403-отказа. Тело ответа (SSE) идёт насквозь чанками.
csrf_token при любом входе получает срок max(JWT_USER_TTL, JWT_ADMIN_TTL): вход в админку больше не сокращает токен игрока. Ротация токена при входе сохраняется.
Проверка double-submit не ослабляется. Запрос с cookie, но без заголовка или с чужим токеном — по-прежнему 403, cookie при этом не перевыдаётся.
Фронт и эндпойнты не меняются.
Тесты backend/tests/test_csrf.py: восстановление токена на GET и на отказе, срок после входа админа, неизменная строгость проверки, анонимам токен не выдаётся, middleware не склеивает чанки потока.
Отдельным коммитом — правка 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`
Итог: если сессия есть, а 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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Что происходит
На проде (после выкладки 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 и отвечает 403CSRF_FAILEDна каждую мутацию. Новыйcsrf_tokenсервер выдаёт только при следующем входе, поэтому само состояние не проходит.Воспроизведение (TestClient, проверено)
POST /api/auth/dev/login→csrf_tokenсMax-Age=604800.POST /api/admin/auth/login→csrf_tokenперезаписан сMax-Age=28800.csrf_tokenиз jar (имитация истечения 8 часов).GET /api/users/me→ 200 (сессия жива);PATCH /api/users/me/profile→ 403CSRF_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./api/eventsпродолжает стримиться (без буферизации ответа).План выполнения
Уточнение к описанию: обход «выйти и войти» не работает. Выход — тоже POST, и его блокирует та же проверка. До исправления поможет только удаление cookie сайта в браузере.
CSRFMiddleware(чистый ASGI): если запрос к/apiнесёт сессионную cookie, аcsrf_tokenнет, в стартовое сообщение ответа дописываетсяSet-Cookieсо свежим токеном. Это касается и 403-отказа. Тело ответа (SSE) идёт насквозь чанками.csrf_tokenпри любом входе получает срокmax(JWT_USER_TTL, JWT_ADMIN_TTL): вход в админку больше не сокращает токен игрока. Ротация токена при входе сохраняется.backend/tests/test_csrf.py: восстановление токена на GET и на отказе, срок после входа админа, неизменная строгость проверки, анонимам токен не выдаётся, middleware не склеивает чанки потока.README.mdвладельца (деплой на Pi →scripts/build-push.ps1).Критерии готовности
csrf_tokenвосстанавливается первым же запросом к/api; мутации и выход после этого проходят.csrf_tokenкороче срокаfs_session./api/eventsстримится без буферизации;pytestзелёный.Ветка:
issue-50-csrf-token-refreshотdevРабота выполнена, открыт 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.