diff --git a/apps/backend/app/api/middleware.py b/apps/backend/app/api/middleware.py index 0a01192..1908e76 100644 --- a/apps/backend/app/api/middleware.py +++ b/apps/backend/app/api/middleware.py @@ -16,6 +16,9 @@ EN_TETES: Final[dict[str, str]] = { "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Referrer-Policy": "no-referrer", + # same-origin : aucun client ne charge l'API en no-cors depuis une autre origine + # (proxy.conf.json en dev, reverse proxy nginx ensuite, cf. docs/architecture/20-backend.md). + "Cross-Origin-Resource-Policy": "same-origin", } PREFIXE_AUTHENTIFICATION: Final = "/auth" diff --git a/apps/backend/tests/api/test_hardening.py b/apps/backend/tests/api/test_hardening.py index 3ee1170..069bcdc 100644 --- a/apps/backend/tests/api/test_hardening.py +++ b/apps/backend/tests/api/test_hardening.py @@ -23,8 +23,9 @@ async def interroge( ("x-content-type-options", "nosniff"), ("x-frame-options", "DENY"), ("referrer-policy", "no-referrer"), + ("cross-origin-resource-policy", "same-origin"), ], - ids=["nosniff", "anti_iframe", "referrer"], + ids=["nosniff", "anti_iframe", "referrer", "corp"], ) async def test_every_response_carries_the_security_headers( client: AsyncClient, entete: str, valeur: str diff --git a/docs/architecture/20-backend.md b/docs/architecture/20-backend.md index 1b6f9fc..cc4411b 100644 --- a/docs/architecture/20-backend.md +++ b/docs/architecture/20-backend.md @@ -423,8 +423,13 @@ Le reste, par ordre de surface : écriture des journaux. C'est la troisième ligne de défense : la première est de ne rien passer de secret au logger, la deuxième de ne jamais mettre un jeton dans une URL. - En-têtes posés par l'application : `X-Content-Type-Options`, `X-Frame-Options`, - `Referrer-Policy`, plus `Cache-Control: no-store` sur `/auth/*`. HSTS et CSP appartiennent au - terminateur TLS, que l'application ne connaît pas : le reverse proxy les pose + `Referrer-Policy`, `Cross-Origin-Resource-Policy: same-origin`, plus `Cache-Control: no-store` + sur `/auth/*`. Le CORP est fixé à `same-origin` parce qu'aucun client légitime ne charge l'API + en `no-cors` (image, script, média) depuis une autre origine : le frontend l'appelle en relatif + (`/api/v1`), sur sa propre origine, via `proxy.conf.json` en dev et le reverse proxy nginx + (`infra/proxy/conf.d/enervision.conf`) en recette et en production. Les appels `HttpClient`, en + mode `cors`, n'y sont de toute façon pas soumis. HSTS et CSP appartiennent au terminateur TLS, que + l'application ne connaît pas : le reverse proxy les pose ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). - Le conteneur tourne en utilisateur non-root, avec un `HEALTHCHECK` sur `/api/v1/health/live`. - TLS, limitation de débit au frontal et journal d'accès sont portés par le reverse proxy. diff --git a/docs/architecture/owasp-traceabilite.md b/docs/architecture/owasp-traceabilite.md index 7202cfa..15618ad 100644 --- a/docs/architecture/owasp-traceabilite.md +++ b/docs/architecture/owasp-traceabilite.md @@ -39,7 +39,7 @@ lecture seule ; plusieurs lignes resteront à compléter une fois les endpoints | Cinq gardes de configuration qui refusent le démarrage plutôt que de dégrader silencieusement | `app/core/config.py` | A05 | | Documentation interactive fermée hors développement, `/metrics` derrière un jeton, sonde qui ne publie plus de version | `app/main.py`, `app/api/security.py` | A05 | | Scan dynamique OWASP ZAP de l'API authentifiée (compte `lecteur` jetable), non bloquant, configuration par défaut du backend uniquement (ni TLS ni en-têtes du reverse proxy) | `.github/workflows/dast.yml`, `scripts/dast-token.sh` | A05, API8 Security Misconfiguration | -| En-têtes `nosniff`, `DENY`, `no-referrer`, et `no-store` sur les routes d'authentification | `app/api/middleware.py` | A05 | +| En-têtes `nosniff`, `DENY`, `no-referrer`, `Cross-Origin-Resource-Policy: same-origin`, et `no-store` sur les routes d'authentification | `app/api/middleware.py` | A05 | | Refus de rétrograder ou désactiver le dernier administrateur actif | `app/services/user.py` | A04 Insecure Design | | Amorçage du premier administrateur hors dépôt, mot de passe jamais dans `argv` ni dans Git | `app/cli.py` | A02, A05 | | Réponse de l'API Mock bornée avant écriture : timeout, plafond de sites et de mesures, bornes physiques par grandeur, recopie des seuls champs attendus | `app/etl/mock_api_import.py` | API10 Unsafe Consumption of APIs |