Files
bare/docs/decisions/021-sessions-csrf-limits.md
T
mayatnikovandClaude Opus 5 8f67f4aa4d Этап 4: PWA и пуши — service worker, Web Push с VAPID, баннер установки
Сервер: пакет push на webpush-go (третья и последняя прямая зависимость),
подписка устройства, правило ADR-023 «пуш только молчащему устройству и только
один» через атомарный захват push_pending, удаление подписки на 404 и 410,
TTL сутки, urgency normal. В нагрузке только {title, body, chat} — тело
собирается из константы, плейнтекст туда не попадает даже по ошибке.

Клиент: service worker с версионированным кэшем оболочки и никогда — /api/*,
push и notificationclick, подписка на VAPID-ключ сервера, запрос разрешения
после первого отправленного сообщения, разделы настроек «уведомления»
и «установить приложение», баннер установки на iOS.

ADR-045: пуш адресован получателю — по букве ADR-023 он уходил бы и молчащему
устройству отправителя с бессмысленным заголовком из собственного ника.
ADR-047: сервер ходит на endpoint подписки, который выбирает браузер. Проверка
«только https» обходилась редиректом, а имя могло смотреть внутрь сети — теперь
запрет редиректов и проверка разрешённого адреса на уровне сокета.
ADR-048: пределы отправки — недоступный push-сервис одного аккаунта больше
не съедает пуши всего сервера.
ADR-049: явно выключенные уведомления сами не включаются обратно.

Попутно: webpush-go дописывает набивку в переданный срез, а одна нагрузка
уходила всем устройствам сообщения — гонка, пойманная go test -race.
Теперь у каждого задания своя копия.

Приёмка на боевом: подписки, hasPush, чужое устройство, Origin, оболочка
из девяти файлов, Service-Worker-Allowed. Отдельно шесть непубличных адресов
и endpoint на 3 КиБ — все отбиты.

Чеклист ручной проверки на iPhone и Android — в docs/plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-23 00:50:58 +03:00

31 lines
3.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-021: Сессии, CSRF, Argon2id и лимиты
## Контекст
ADR-005 задаёт «Argon2id, сессия в httpOnly cookie» без параметров. Cookie плюс `fetch POST` — классическая поверхность для CSRF. Лимиты и защита от перебора — открытый вопрос.
## Решение
**Argon2id.** Вход — `authKey` (32 случайных байта с точки зрения сервера, ADR-015), поэтому параметры умеренные: memory 19 MiB, time 2, parallelism 1, соль 16 байт, выход 32 байта. Параметры записываются рядом с хешем; повышение — перехеш при очередном входе.
**Сессия.** Токен — 32 случайных байта; в базе хранится `SHA-256(токен)`. Cookie `bare_session`: `HttpOnly; Secure; SameSite=Strict; Path=/`; срок 90 дней без продления. После регистрации устройства сессия привязывается к нему. `POST /api/logout` удаляет сессию; смена пароля по желанию завершает остальные; удаление устройства завершает его сессии.
**CSRF.** Два независимых барьера: `SameSite=Strict` и проверка заголовка `Origin` на всех запросах кроме `GET`/`HEAD` — он обязан равняться `BARE_ORIGIN`. Приложение живёт на одном origin, сторонних встраиваний нет.
**Заголовки.** `Content-Security-Policy: default-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'self'`. Никаких inline-скриптов и inline-стилей — CSP их запрещает, и это правило для клиентского кода. `Referrer-Policy: no-referrer`, `X-Content-Type-Options: nosniff`, HSTS на nginx.
**Лимиты.** Token bucket в памяти сервера:
- регистрация — 5 в час на IP;
- вход — 10 за 10 минут на пару IP+ник;
- сообщения — 30 в минуту на пользователя, пакет 10;
- остальные изменяющие запросы — 60 в минуту на пользователя.
Превышение — `429` с `Retry-After`. IP берётся из `X-Real-IP`, только если соединение с `127.0.0.1` (nginx, ADR-022).
**Размеры.** Тело запроса — до 32 КиБ, текст сообщения — до 4000 символов, имя комнаты — до 64, ник — до 32. Имя комнаты вдобавок не бывает пустым, из одних пробелов, с управляющими символами и с переопределениями направления письма (U+202A…U+202E, U+2066…U+2069): с этапа 4 оно уходит в заголовок системного уведомления (ADR-045), а там перевод строки и разворот текста выдают чужое имя за сообщение системы.
## Следствия
- Сторонний сайт не может ни отправить сообщение, ни выйти из аккаунта от имени пользователя.
- Лимиты живут в памяти: рестарт их обнуляет. Для маленького сервера это приемлемо.
- 90-дневный вход без продления — раз в квартал пароль вводится заново на каждом устройстве.