Хардненинг по итогам пен-теста #25 (#56–#62) #63
+7
-3
@@ -151,12 +151,16 @@ async def _lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
|
|
||||||
def create_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(
|
app = FastAPI(
|
||||||
title="Forbidden Stars API",
|
title="Forbidden Stars API",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
openapi_url="/api/openapi.json",
|
openapi_url="/api/openapi.json" if docs_enabled else None,
|
||||||
docs_url="/api/docs",
|
docs_url="/api/docs" if docs_enabled else None,
|
||||||
redoc_url="/api/redoc",
|
redoc_url="/api/redoc" if docs_enabled else None,
|
||||||
lifespan=_lifespan,
|
lifespan=_lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
+20
-1
@@ -18,8 +18,27 @@
|
|||||||
# иначе браузер отдаёт старый index.html из кэша и до Caddy/заглушки запрос не доходит.
|
# иначе браузер отдаёт старый index.html из кэша и до Caddy/заглушки запрос не доходит.
|
||||||
# Файл заглушки — deploy/vps/maintenance.html.
|
# Файл заглушки — deploy/vps/maintenance.html.
|
||||||
|
|
||||||
# Edge-поведение, общее для сайтов: запрет кэша HTML-документа + заглушка при падении апстрима.
|
# Edge-поведение, общее для сайтов: security-заголовки + запрет кэша HTML + заглушка при падении апстрима.
|
||||||
(edge) {
|
(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) НЕ кэшируем. Иначе браузер отдаёт старый
|
# HTML-документ (навигации, Accept: text/html) НЕ кэшируем. Иначе браузер отдаёт старый
|
||||||
# SPA из кэша без сетевого запроса → запрос не доходит до Caddy и заглушку не видно.
|
# SPA из кэша без сетевого запроса → запрос не доходит до Caddy и заглушку не видно.
|
||||||
# Хэшированные ассеты (JS/CSS) под это не попадают (у них другой Accept) и кэшируются как обычно.
|
# Хэшированные ассеты (JS/CSS) под это не попадают (у них другой Accept) и кэшируются как обычно.
|
||||||
|
|||||||
Reference in New Issue
Block a user