SECURITY.md
Rama main · réplica de lectura del repositorio original.
# Безопасность / Security
## Как сообщить об уязвимости
Не создавайте публичный issue. Отправьте приватный отчёт через GitHub: **Security → Report a vulnerability** в [artkruglov/polka](https://github.com/artkruglov/polka/security/advisories/new).
Укажите затронутую версию или commit, установку (polochka.app, локальный запуск или своя), шаги воспроизведения и возможный ущерб. Не прикладывайте чужие данные, действующие токены и ссылки `/s#…`. Мы подтвердим получение в течение 5 рабочих дней и сообщим план исправления. Пожалуйста, не раскрывайте детали публично, пока не выйдет исправление.
*English: please report vulnerabilities privately via GitHub Security Advisories (link above), not in a public issue. We acknowledge reports within 5 business days.*
Проверяйте только на своей установке или на своём аккаунте polochka.app. Не проводите нагрузочных тестов, не обращайтесь к чужим данным и не пытайтесь обойти защиту Cloudflare и других сторонних сервисов.
## Поддерживаемые версии
| Версия | Исправления безопасности |
|---|---|
| `main` и последний выпуск `0.12.x` | да |
| Более старые prerelease | нет, обновитесь |
## Модель угроз вкратце
Полная картина описана в [docs/architecture.md](docs/architecture.md#границы-доверия).
- **Чужой HTML считается враждебным.** По умолчанию страница показывается статично: CSP без скриптов и без сети. Интерактивный режим (`HTML_LIVE_MODE=production`) требует отдельного registrable domain для viewer (на polochka.app это polochka.page). Страница работает в `sandbox` без `allow-same-origin`, у неё строгая CSP без сети, и получает она только короткоживущий грант на конкретную собранную производную. Cookies и API Полки ей недоступны. Не включайте интерактивный режим на домене приложения: в режимах staging и production конфигурация такое отклоняет. Если домен просмотра настроен, с него отдаётся и статичный просмотр, а домен приложения пользовательский HTML не показывает вовсе. Внешние ссылки статичного просмотра ведут на страницу Полки «Вы уходите с Полки» с подписанным адресом; без подписи она ничего не открывает, поэтому открытым редиректом не служит. CSP не закрывает WebRTC, поэтому просмотрщик первым скриптом каждой страницы, в том числе каждой страницы проекта, убирает конструкторы RTC. Частичная приёмка изоляции на hosted прошла 23.09.2026 в Chrome 153 (протокол — `docs/reviews/2026-09-23-viewer-egress` в приватном архиве оператора, проба — `scripts/viewer-egress-probe.mjs`); Firefox, Safari и независимый счётчик egress ещё не проверены. Контракты: [LIVE_VIEWER_SPEC](docs/LIVE_VIEWER_SPEC.md), [HOSTED_VIEWER_DELTA](docs/HOSTED_VIEWER_DELTA.md).
- **Сборка чужого кода ограничена.** Runtime-сборщик работает в worker thread с кучей 64 МБ и сроком 5 секунд, в одном процессе выполняется одна runtime-сборка за раз. У esbuild лимит памяти (`ulimit -d`, соблюдается только на Linux), и загружает он только файлы страницы и разрешённых библиотек (проверка в плагине сборки). Разрешены только библиотеки из `packages/contracts/runtime.ts`, размер выхода не больше 8 МиБ, исходная страница не больше 5 МиБ.
- **Права агента задаются при подключении.** Токен подключения несёт scopes (`context`, `capture`, `read`, `source:read`, `revise`, `share`, `manage`, `sign_in`), в БД хранятся только SHA-256. На странице согласия OAuth по умолчанию отмечены **все** scopes, включая выпуск ссылок (`share`), корзину и папки (`manage`) и ссылку для входа во временную полку (`sign_in`) — решение владельца от 25.09.2026, чтобы агент работал без настройки. Владелец может снять любой из них до «Разрешить» или позже отозвать подключение. Действия за этими scopes обратимы (корзина, новые версии не стирают старые, удаляются только пустые папки), а ссылка для входа открывает только временную полку и даёт слабую сессию. Отзыв действует сразу. Уже выданные ссылки отзыв подключения не закрывает.
- **OAuth 2.1 для чат-приложений.** Только authorization code с PKCE S256; resource indicator, если передан, должен указывать на `/mcp` этой установки; токены всегда выпускаются с audience `/mcp`. Код одноразовый и живёт 60 секунд. Повтор refresh-токена отзывает подключение целиком. Решение на странице согласия требует сессию, CSRF и `Origin` Полки. Неподтверждённые имена клиентов помечаются. См. [docs/MCP_CONNECTOR.md](docs/MCP_CONNECTOR.md).
- **Вход по коду из письма.** Код восьмизначный, действует 10 минут, привязан к браузеру, где вход начат, и даёт 5 попыток; на один адрес — не больше 3 кодов за 10 минут. Блокировки адреса нет: её мог бы включить посторонний. При таких лимитах перебор даёт меньше 1% за год непрерывной атаки, и каждая её попытка приходит владельцу письмом.
- **Publish API** (`/api/v1/publish`) принимает только bearer-токен, cookies не читает и отклоняет чужой `Origin`. Частота запросов ограничена по токену; по IP считаются только запросы без токена или с неверным токеном. См. [docs/PUBLISH_API.md](docs/PUBLISH_API.md).
- **Ссылки.** Секрет передаётся во фрагменте URL и не попадает в логи сервера. Ссылка привязана к версии, у неё есть срок и отзыв. **Скачанные копии отозвать нельзя.**
- **За прокси** задайте `TRUST_PROXY`, иначе все лимиты по IP (попытки входа, регистрации, запросы без токена) будут общими для всех клиентов: сервер увидит один адрес прокси. DNS-запись приложения не должна проксироваться через CDN: CDN увидел бы токены агентов и мог бы менять HTML.
- **Выключено по умолчанию:** импорт по URL (`URL_IMPORT_ENABLED`; при включении защищён от SSRF закреплением DNS, проверкой redirect и запретом частных диапазонов) и удаление аккаунта (`ACCOUNT_DELETION_ENABLED`, для hosted-установки не принято).