From 750a65d5ee91c7c28938c59b7f4cfa16448712f0 Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Sun, 2 Aug 2026 23:12:50 +0300 Subject: [PATCH] docs: move the accepted security risks into docs/security.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plan holds undone work; an accepted risk is a decision, not a task — it has no place in a queue, only a condition for revisiting it. Both risks (POST with neither Sec-Fetch-Site nor Origin, no session-bound CSRF tokens) move verbatim into a new docs/security.md, which also states where D.5 findings land. Section letters and item numbering in the plan stay as they were, since progress.md and the commit history reference them; a note in their place points at the new file. Co-Authored-By: Claude Opus 5 --- README.md | 5 +++-- docs/implementation-plan.md | 27 +++++-------------------- docs/progress.md | 7 ++++--- docs/security.md | 39 +++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 docs/security.md diff --git a/README.md b/README.md index 92941b8..3b9389f 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ signing, and is configured once through the panel. It is **outbound only** — i does not receive mail, provide mailboxes, or offer webmail. > **Status: under active development.** See [docs/specification.md](docs/specification.md) -> for the full requirements and [docs/implementation-plan.md](docs/implementation-plan.md) -> for the phased build plan. +> for the full requirements, [docs/implementation-plan.md](docs/implementation-plan.md) +> for the phased build plan, and [docs/security.md](docs/security.md) for the +> security trade-offs that were accepted knowingly. ## Requirements (site checklist) diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index c346e0b..4005136 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -17,27 +17,10 @@ пункт — решение «делаем в v1.x / откладываем в 2.x / оставляем как есть», принимается пользователем. -### A. Безопасность — принятые риски - -Hardening сверх обязательного 7.6 закрыт. Здесь остаётся только то, что закрыто -**сознательно не было**, чтобы это не потерялось: - -- **Принятый риск: `POST` без `Sec-Fetch-Site` и без `Origin` пропускается.** - Клиент, не посылающий ни одного из двух — по-настоящему старый браузер или - webview с замороженным движком, — остаётся уязвим к CSRF с любого сайта. - Принято сознательно: панель однопользовательская, админ выбирает браузер - сам, а строгий режим не «защитил бы» такой клиент, а просто сломал бы в нём - панель. Ужесточение — одна строка в `originAllowed` - ([internal/web/security.go](../internal/web/security.go)): вернуть `false` - вместо `true` в ветке «нет обоих заголовков». -- **CSRF-токены, привязанные к сессии, не делаются.** Проверка origin - закрывает соседний поддомен, но зависит от поведения браузера; токен — нет. - Цена — скрытое поле примерно в двух десятках форм. Триггером вернуться к - вопросу считать появление требования «устойчиво независимо от браузера». - От XSS внутри самой панели не спас бы и токен: код, исполняющийся в origin - панели, отправит запрос сам — против этого работают автоэкранирование - `html/template` (7.6.7) и CSP, поэтому шаблоны не должны содержать - inline-скриптов и inline-стилей. +**Раздел A (принятые риски безопасности) переехал в +[security.md](security.md)** — риск не задача, а решение, и в плане несделанной +работы ему делать нечего. Буквы разделов и сквозная нумерация пунктов ниже +оставлены как были: на них ссылаются `progress.md`, коммиты и обсуждения. ### B. Надёжность и эксплуатация @@ -132,7 +115,7 @@ Hardening сверх обязательного 7.6 закрыт. Здесь о **Модель — Fable, и это сознательно не Opus:** B и C пишет Opus, а проверка собственной работы систематически слабее независимой. Правило progress.md «безопасность/инфра → Opus» этим не отменяется — оно про написание кода, здесь речь про ревизию. Форма прогона: `/security-review` по изменениям, пока они ещё в ветке (скилл смотрит диф), плюс отдельный ручной проход по 7.6 целиком. - **Что с находками.** Каждая закрывается явно: правка до тега либо запись в раздел A как принятый риск с обоснованием (там уже два таких). «Посмотрели и ладно» закрытием не считается. **Гейт:** вместе с e2e из C.4 — до тега; находка класса «эксплуатируется снаружи» тег откладывает. + **Что с находками.** Каждая закрывается явно: правка до тега либо запись в [security.md](security.md) как принятый риск с обоснованием (там уже два таких). «Посмотрели и ладно» закрытием не считается. **Гейт:** вместе с e2e из C.4 — до тега; находка класса «эксплуатируется снаружи» тег откладывает. ### E. Указатель на объём 2.x diff --git a/docs/progress.md b/docs/progress.md index 3781150..eddd32b 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -2,13 +2,14 @@ Живой трекер состояния. **Переживает `/clear`** — читается первым при возобновлении работы. План (открытые вопросы + опциональные фазы): [implementation-plan.md](implementation-plan.md). -ТЗ: [specification.md](specification.md). История релизов: [CHANGELOG.md](../CHANGELOG.md). +ТЗ: [specification.md](specification.md). Принятые риски безопасности: [security.md](security.md). +История релизов: [CHANGELOG.md](../CHANGELOG.md). История сделанного по фазам (0→13, все закрыты) — в `git log` и в CHANGELOG, здесь не дублируется. ## Как возобновить после сброса контекста 1. Прочитать этот файл (текущее состояние, что дальше). -2. Открыть `implementation-plan.md` — там нерешённые вопросы, принятые риски и опциональная линия 2.x.x (Фаза O1+). +2. Открыть `implementation-plan.md` — там нерешённые вопросы и опциональная линия 2.x.x (Фаза O1+); принятые риски безопасности — в `security.md`. 3. При необходимости — детали в `specification.md`. 4. Продолжить с пункта «Следующий шаг». @@ -38,7 +39,7 @@ - **Выполнено и принято:** базовый линейный план 0→11 (v1.0; аудит безопасности ТЗ 7.6 — полное соответствие), Фаза 12 (UI/UX), Фаза 13 (страница `/status`, DNS-проверки домена) и Фаза 14 (security-заголовки, проверка origin, cookie `__Host-` + обнаружение дублей, документация про `/data/setup-token`). Что именно сделано — в `git log` и `CHANGELOG.md`, здесь не дублируется. - **Решено, но ещё не реализовано:** пункты **B.1**, **B.2**, **B.3** и **C.4** плана, именно в этом порядке. B.1 — сессии переезжают в SQLite (скользящий срок бездействия 7 дней, `PANEL_SESSION_IDLE_DAYS`, опросы не продлевают, смена пароля разлогинивает всех). B.2 — ротация `mail.log` уходит с `copytruncate` на «переименовать + `postfix reload`» (правки в `logrotate-mail.conf`, `follow()` в `internal/logtail`, `readLogTail()` в `internal/web`; на стенде проверить, что после reload новый `mail.log` создаётся). B.3 — незаданный `SELFPOST_HOSTNAME` роняет контейнер в `entrypoint.sh` с развёрнутым текстом ошибки плюс синтаксическая проверка значения. C.4 — герметичный контейнерный e2e отдельным Go-модулем `test/e2e/` поверх поставляемого compose, гейт перед публикацией образа по тегу, нативная матрица amd64/arm64 вместо qemu в `release.yml`; делается **после** B.1–B.3, стендовые проверки B.1/B.3 переезжают в него регрессиями. Параметры всех четырёх согласованы полностью, кода нет: делать отдельными шагами. Замыкает очередь **D.5** — предрелизная проверка на уязвимости моделью Fable по всему дифу от `v1.0.0` плюс повторный проход по ТЗ 7.6; вместе с e2e это гейт перед тегом. -- **Дальше — то, что перечислено в `implementation-plan.md`:** открытые вопросы закрыты, раздел E теперь только указатель на объём 2.x (входящий релей O1+ и роль администратора домена; 2FA снята с рассмотрения); остаются принятые риски раздела A (`POST` без `Sec-Fetch-Site`/`Origin` пропускается, токенов нет) и опциональная **Фаза O1+** (входящий релей, линия 2.x.x, требует согласования). +- **Дальше — то, что перечислено в `implementation-plan.md`:** открытые вопросы закрыты, раздел E теперь только указатель на объём 2.x (входящий релей O1+ и роль администратора домена; 2FA снята с рассмотрения); остаются принятые риски безопасности (переехали в [security.md](security.md): `POST` без `Sec-Fetch-Site`/`Origin` пропускается, CSRF-токенов нет) и опциональная **Фаза O1+** (входящий релей, линия 2.x.x, требует согласования). - **Прод:** `selfpost.mixfed.ru`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает). ## Рабочая петля (dev loop) — ВАЖНО diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..f9c2b83 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,39 @@ +# Безопасность: принятые риски + +**Что здесь.** Обязательные требования к безопасности — [ТЗ 7.6](specification.md); +соответствие им проверено полным аудитом на v1.0 и здесь не пересказывается. +Hardening сверх обязательного 7.6 (security-заголовки, проверка origin, cookie +`__Host-` с обнаружением дублей — Фаза 14) тоже закрыт, история — в +[CHANGELOG.md](../CHANGELOG.md) и `git log`. Этот документ держит третью +категорию: то, что закрыто **сознательно не было**, чтобы решение не потерялось +и не переоткрывалось заново. + +Здесь, а не в [implementation-plan.md](implementation-plan.md), потому что план — +только про несделанную работу, а принятый риск — не работа, а решение: у него нет +состояния «в очереди», есть условие, при котором к нему возвращаются. + +## Принятые риски + +- **`POST` без `Sec-Fetch-Site` и без `Origin` пропускается.** + Клиент, не посылающий ни одного из двух — по-настоящему старый браузер или + webview с замороженным движком, — остаётся уязвим к CSRF с любого сайта. + Принято сознательно: панель однопользовательская, админ выбирает браузер + сам, а строгий режим не «защитил бы» такой клиент, а просто сломал бы в нём + панель. Ужесточение — одна строка в `originAllowed` + ([internal/web/security.go](../internal/web/security.go)): вернуть `false` + вместо `true` в ветке «нет обоих заголовков». +- **CSRF-токены, привязанные к сессии, не делаются.** Проверка origin + закрывает соседний поддомен, но зависит от поведения браузера; токен — нет. + Цена — скрытое поле примерно в двух десятках форм. Триггером вернуться к + вопросу считать появление требования «устойчиво независимо от браузера». + От XSS внутри самой панели не спас бы и токен: код, исполняющийся в origin + панели, отправит запрос сам — против этого работают автоэкранирование + `html/template` (7.6.7) и CSP, поэтому шаблоны не должны содержать + inline-скриптов и inline-стилей. + +## Как этот список пополняется + +Предрелизная проверка на уязвимости (пункт **D.5** плана, модель Fable) закрывает +каждую находку одним из двух способов: правка до тега — либо запись сюда, с +обоснованием и условием возврата, как у двух пунктов выше. Третьего варианта +(«посмотрели и ладно») нет.