From ce7fbad58ed2d2534fac8267ea136dded71b366b Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Sun, 13 Sep 2026 17:19:42 +0300 Subject: [PATCH] =?UTF-8?q?API:=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D1=85=D0=B5=D0=BC=D1=83=20=D0=B2=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=20+=20security-=D0=B7=D0=B0=D0=B3=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BD=D0=B0=20edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.py: openapi.json/docs/redoc отдаются только в dev/test (нужны для gen:api), в production отключены. Caddyfile (edge): HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, скрыт Server; CSP подготовлена, но выключена до проверки на test-клоне (строгая политика ломает SPA/Telegram-виджет/SSE). #61 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6 --- backend/app/main.py | 10 +++++++--- backend/tests/test_api_hardening.py | 22 ++++++++++++++++++++++ deploy/vps/Caddyfile | 21 ++++++++++++++++++++- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 backend/tests/test_api_hardening.py diff --git a/backend/app/main.py b/backend/app/main.py index d58e61b..b91cb9c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -151,12 +151,16 @@ async def _lifespan(_app: FastAPI): def create_app() -> FastAPI: + # Схему API (openapi.json + Swagger/ReDoc) отдаём только в dev/test: она нужна для + # `npm run gen:api` (генерация типов фронта) и удобной отладки. В production закрываем — + # незачем облегчать разведку поверхности API анонимам (#61). + docs_enabled = not settings.is_production app = FastAPI( title="Forbidden Stars API", version="0.1.0", - openapi_url="/api/openapi.json", - docs_url="/api/docs", - redoc_url="/api/redoc", + openapi_url="/api/openapi.json" if docs_enabled else None, + docs_url="/api/docs" if docs_enabled else None, + redoc_url="/api/redoc" if docs_enabled else None, lifespan=_lifespan, ) diff --git a/backend/tests/test_api_hardening.py b/backend/tests/test_api_hardening.py new file mode 100644 index 0000000..4de3185 --- /dev/null +++ b/backend/tests/test_api_hardening.py @@ -0,0 +1,22 @@ +"""Хардненинг API: раскрытие схемы закрыто в production, открыто в dev/test (#61, F6).""" +from __future__ import annotations + +from fastapi.testclient import TestClient + +from app.core import config +from app.main import create_app + + +def test_openapi_open_in_development(client: TestClient): + # Тесты идут в development (conftest) — схема доступна: нужна для `npm run gen:api`. + assert client.get("/api/openapi.json").status_code == 200 + assert client.get("/api/docs").status_code == 200 + + +def test_openapi_closed_in_production(monkeypatch): + monkeypatch.setattr(config.settings, "app_env", "production") + prod_app = create_app() + c = TestClient(prod_app) + assert c.get("/api/openapi.json").status_code == 404 + assert c.get("/api/docs").status_code == 404 + assert c.get("/api/redoc").status_code == 404 diff --git a/deploy/vps/Caddyfile b/deploy/vps/Caddyfile index e469638..09cc82c 100644 --- a/deploy/vps/Caddyfile +++ b/deploy/vps/Caddyfile @@ -18,8 +18,27 @@ # иначе браузер отдаёт старый index.html из кэша и до Caddy/заглушки запрос не доходит. # Файл заглушки — deploy/vps/maintenance.html. -# Edge-поведение, общее для сайтов: запрет кэша HTML-документа + заглушка при падении апстрима. +# Edge-поведение, общее для сайтов: security-заголовки + запрет кэша HTML + заглушка при падении апстрима. (edge) { + # Security-заголовки (#61). HSTS — принудительный HTTPS на год с поддоменами; nosniff — + # запрет MIME-sniffing; frame DENY — защита от кликджекинга (наши страницы нельзя встроить + # в чужой iframe); Referrer/Permissions — минимизация утечек. Server скрываем, чтобы не + # светить используемый прокси. + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + Permissions-Policy "geolocation=(), microphone=(), camera=()" + -Server + } + + # Content-Security-Policy подготовлена, но ВЫКЛЮЧЕНА до проверки на test-клоне: строгая + # политика легко ломает SPA (инлайновые стили Vite), Telegram-виджет входа (скрипт с + # telegram.org + iframe oauth.telegram.org) и EventSource (/api/events). Раскомментировать + # после проверки на forbidden-stars.ru, что вход и реал-тайм работают (#61). + # header Content-Security-Policy "default-src 'self'; script-src 'self' https://telegram.org https://oauth.telegram.org; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-src https://oauth.telegram.org; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" + # HTML-документ (навигации, Accept: text/html) НЕ кэшируем. Иначе браузер отдаёт старый # SPA из кэша без сетевого запроса → запрос не доходит до Caddy и заглушку не видно. # Хэшированные ассеты (JS/CSS) под это не попадают (у них другой Accept) и кэшируются как обычно.