fix: injecte le nonce CSP dans les pages /docs et /redoc générées par FastAPI (BUG-106)
This commit is contained in:
+13
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.50.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.50.1**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,18 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.50.1] — 2026-10-02
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
|
||||||
|
- **BUG-106 — page blanche sur `/docs`** : la CSP `script-src` sans
|
||||||
|
`unsafe-inline` (#87 T5c) refusait le script inline d'init de Swagger UI,
|
||||||
|
généré par FastAPI et jamais noncé — `SecurityHeadersMiddleware` injecte
|
||||||
|
désormais le nonce dans le HTML de `/docs` et `/redoc` (helper existant
|
||||||
|
`inject_csp_nonce`, corps ré-encodé gzip), avec 3 tests de non-régression.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.50.0] — 2026-10-02
|
## [2.50.0] — 2026-10-02
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.50.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.50.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.50.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.50.1 | Dernière mise à jour : Septembre 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.50.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.50.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.50.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.50.1 | Last updated: September 2026*
|
||||||
|
|||||||
+25
-1
@@ -167,7 +167,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|||||||
"""Add security headers to all HTTP responses."""
|
"""Add security headers to all HTTP responses."""
|
||||||
|
|
||||||
async def dispatch(self, request, call_next):
|
async def dispatch(self, request, call_next):
|
||||||
from backend.csp import new_nonce
|
from backend.csp import inject_csp_nonce, new_nonce
|
||||||
|
|
||||||
# Nonce CSP frais par réponse (#87 T5b) : injecté dans script-src et
|
# Nonce CSP frais par réponse (#87 T5b) : injecté dans script-src et
|
||||||
# dans le balisage HTML par les routes (backend.csp.inject_csp_nonce).
|
# dans le balisage HTML par les routes (backend.csp.inject_csp_nonce).
|
||||||
@@ -200,6 +200,30 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|||||||
"form-action 'self'; "
|
"form-action 'self'; "
|
||||||
"frame-ancestors 'self';"
|
"frame-ancestors 'self';"
|
||||||
)
|
)
|
||||||
|
# BUG-106 : /docs et /redoc sont générés par FastAPI, hors de nos
|
||||||
|
# routes qui appellent inject_csp_nonce — leur balisage inline
|
||||||
|
# restait sans nonce et était bloqué par script-src (page blanche).
|
||||||
|
# On injecte ici avec le même helper, seul le HTML est retouché.
|
||||||
|
if request.url.path.startswith(("/docs", "/redoc")) and "text/html" in (
|
||||||
|
response.headers.get("Content-Type") or ""
|
||||||
|
):
|
||||||
|
import gzip
|
||||||
|
|
||||||
|
# La compression (GZipMiddleware) est plus proche de la route :
|
||||||
|
# le corps arrive déjà gunzippé, on recomprime à l'identique.
|
||||||
|
raw = b"".join([chunk async for chunk in response.body_iterator])
|
||||||
|
is_gz = "gzip" in (response.headers.get("Content-Encoding") or "")
|
||||||
|
if is_gz:
|
||||||
|
raw = gzip.decompress(raw)
|
||||||
|
patched = inject_csp_nonce(raw.decode("utf-8", "replace"), nonce).encode("utf-8")
|
||||||
|
if is_gz:
|
||||||
|
patched = gzip.compress(patched)
|
||||||
|
response.headers["Content-Length"] = str(len(patched))
|
||||||
|
|
||||||
|
async def _docs_body():
|
||||||
|
yield patched
|
||||||
|
|
||||||
|
response.body_iterator = _docs_body()
|
||||||
# Static assets are NOT content-hashed, so they must revalidate:
|
# Static assets are NOT content-hashed, so they must revalidate:
|
||||||
# ``immutable``/long max-age made Cloudflare and mobile browsers serve
|
# ``immutable``/long max-age made Cloudflare and mobile browsers serve
|
||||||
# a stale build for a year (the service worker cache compounded it).
|
# a stale build for a year (the service worker cache compounded it).
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.50.0"
|
version = "2.50.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.50.0"
|
version = "2.50.1"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.50.0",
|
"version": "2.50.1",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| *BUG-103* | Mobile : le bas du sidebar de navigation est masqué par la barre d'outils du bas (`z-index` 200 < 900) | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/style.css` (règle `.sidebar` du bloc `@media (max-width: 768px)`) | Vue mobile (≤768 px), sidebar ouvert depuis « Explorateur » | Passé à `z-index: 950` (au-dessus de `.mobile-toolbar`, 900) | Exposé après #158 (retour utilisateur). Vérifié : `elementFromPoint` Playwright (393×851) + test source `mobile sidebar over toolbar` (`unit.test.mjs`) |
|
| *BUG-103* | Mobile : le bas du sidebar de navigation est masqué par la barre d'outils du bas (`z-index` 200 < 900) | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/style.css` (règle `.sidebar` du bloc `@media (max-width: 768px)`) | Vue mobile (≤768 px), sidebar ouvert depuis « Explorateur » | Passé à `z-index: 950` (au-dessus de `.mobile-toolbar`, 900) | Exposé après #158 (retour utilisateur). Vérifié : `elementFromPoint` Playwright (393×851) + test source `mobile sidebar over toolbar` (`unit.test.mjs`) |
|
||||||
| *BUG-104* | Desktop : toutes les commandes Tauri invoquées depuis la page backend (`http://127.0.0.1`) rejetées « not allowed by ACL » — le bouton « Choisir mon dossier » du wizard ne fait rien | 🟢 corrigé | P1 | 🖥️ desktop | IA | `desktop/permissions/commands.toml` (nouveau), `desktop/capabilities/default.json`, `desktop/build.rs` | Installeur 2.49.2 : clic sur « Choisir mon dossier » → aucune fenêtre ; console `[desktop] invoke(pick_vault_folder) failed: Command … not allowed by ACL` (capturée via CDP, WebView2 `--remote-debugging-port`) | Manifeste de permissions applicatives créé (`allow-app-commands`, 19 commandes) et référencé par la capability : Tauri v2 ACL gate toute commande invoquée depuis une origine *remote*, y compris celles de l'`invoke_handler` | `cargo test` 25 passed (nouveau garde-fou `test_frontend_invokes_are_acl_allowed`) + vérif live CDP : `get_version`→2.49.2, clic → fenêtre native « Select Folder », parcours complet ajout de vault terminé |
|
| *BUG-104* | Desktop : toutes les commandes Tauri invoquées depuis la page backend (`http://127.0.0.1`) rejetées « not allowed by ACL » — le bouton « Choisir mon dossier » du wizard ne fait rien | 🟢 corrigé | P1 | 🖥️ desktop | IA | `desktop/permissions/commands.toml` (nouveau), `desktop/capabilities/default.json`, `desktop/build.rs` | Installeur 2.49.2 : clic sur « Choisir mon dossier » → aucune fenêtre ; console `[desktop] invoke(pick_vault_folder) failed: Command … not allowed by ACL` (capturée via CDP, WebView2 `--remote-debugging-port`) | Manifeste de permissions applicatives créé (`allow-app-commands`, 19 commandes) et référencé par la capability : Tauri v2 ACL gate toute commande invoquée depuis une origine *remote*, y compris celles de l'`invoke_handler` | `cargo test` 25 passed (nouveau garde-fou `test_frontend_invokes_are_acl_allowed`) + vérif live CDP : `get_version`→2.49.2, clic → fenêtre native « Select Folder », parcours complet ajout de vault terminé |
|
||||||
| *BUG-105* | Desktop : crash 2-15 s après l'ouverture de la fenêtre (`APPCRASH c0000374`, heap corruption détectée dans ntdll) — absent de v2.0.0, apparu en 2.49.2 | 🟢 corrigé | P1 | 🖥️ desktop | IA | `desktop/src/jumplist.rs` (`set_link_title`) | Installeur 2.49.2 : fenêtre visible quelques secondes puis fermeture (3/3), Event Log Windows `APPCRASH obsigate-desktop.exe … c0000374`, rien dans stderr | `set_link_title` (#77) construisait un `PROPVARIANT VT_LPWSTR` pointant un buffer heap Rust et le confiait au property store du shell, qui le libère avec l'allocateur Windows → corruption du tas ; fonction supprimée (`SetDescription` fournit déjà le libellé) | Bisect 3 états (jumplist off = stable, titre off = stable, d'origine = crash) ; run 9 min sans crash + sortie propre `EXIT=0`, zéro événement WER (avant : 3 crashes ≤20 s) |
|
| *BUG-105* | Desktop : crash 2-15 s après l'ouverture de la fenêtre (`APPCRASH c0000374`, heap corruption détectée dans ntdll) — absent de v2.0.0, apparu en 2.49.2 | 🟢 corrigé | P1 | 🖥️ desktop | IA | `desktop/src/jumplist.rs` (`set_link_title`) | Installeur 2.49.2 : fenêtre visible quelques secondes puis fermeture (3/3), Event Log Windows `APPCRASH obsigate-desktop.exe … c0000374`, rien dans stderr | `set_link_title` (#77) construisait un `PROPVARIANT VT_LPWSTR` pointant un buffer heap Rust et le confiait au property store du shell, qui le libère avec l'allocateur Windows → corruption du tas ; fonction supprimée (`SetDescription` fournit déjà le libellé) | Bisect 3 états (jumplist off = stable, titre off = stable, d'origine = crash) ; run 9 min sans crash + sortie propre `EXIT=0`, zéro événement WER (avant : 3 crashes ≤20 s) |
|
||||||
|
| *BUG-106* | Page blanche sur `/docs` (Swagger UI) : la CSP `script-src` sans `unsafe-inline` (#87 T5c) refuse le script inline d'init de FastAPI, jamais noncé | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/main.py` (`SecurityHeadersMiddleware`) | Ouvrir `https://og.dracodev.net/docs` : HTML servi (200) mais aucune UI, console `Refused to execute inline script … Content Security Policy` | Injection du nonce dans le HTML de `/docs` et `/redoc` depuis le middleware, via le helper existant `inject_csp_nonce` ; corps décompressé/recompressé (GZipMiddleware est plus proche de la route) | `pytest tests/test_csp_nonce.py` (3 tests ajoutés : nonce sur `/docs`, `/redoc` sans injection, regex sur bundle externe) + 45 passed sur les 3 suites sécurité ; ruff/mypy 0 |
|
||||||
| | | | | | | | | | |
|
| | | | | | | | | | |
|
||||||
### TODOs techniques (améliorations / nouvelles tâches)
|
### TODOs techniques (améliorations / nouvelles tâches)
|
||||||
|
|
||||||
@@ -322,6 +323,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| 2026-10-02 | BUG-103 | Correction | `frontend/style.css` | **BUG-103 — sidebar mobile sous la barre d'outils du bas** : en mobile le `.sidebar` (`z-index: 200`) passait sous `.mobile-toolbar` (`z-index: 900`, 64 px de haut), masquant les dernières entrées de l'arbre ; passé à `950`. Vérifié : `elementFromPoint` Playwright (393×851) + test source `mobile sidebar over toolbar` (`unit.test.mjs`). | 🟢 corrigé |
|
| 2026-10-02 | BUG-103 | Correction | `frontend/style.css` | **BUG-103 — sidebar mobile sous la barre d'outils du bas** : en mobile le `.sidebar` (`z-index: 200`) passait sous `.mobile-toolbar` (`z-index: 900`, 64 px de haut), masquant les dernières entrées de l'arbre ; passé à `950`. Vérifié : `elementFromPoint` Playwright (393×851) + test source `mobile sidebar over toolbar` (`unit.test.mjs`). | 🟢 corrigé |
|
||||||
| 2026-10-02 | BUG-104 | Correction | `desktop/permissions/commands.toml` (nouveau), `desktop/capabilities/default.json`, `desktop/build.rs` | **BUG-104 — commandes Tauri rejetées par l'ACL depuis la page backend** : la fenêtre redirige vers `http://127.0.0.1:<port>`, origine *remote* pour Tauri v2, et aucune commande applicative n'était déclarée → `Command … not allowed by ACL` sur `pick_vault_folder`, `get_wizard_state`, etc. ; les erreurs sont avalées par `desktop.js:invoke()` → bouton du wizard silencieusement inopérant depuis #77. Correctif : manifeste `desktop/permissions/commands.toml` (`allow-app-commands`, 19 commandes) référencé par la capability + `rerun-if-changed=permissions` dans `build.rs`. Vérifié : `cargo test` 25 passed (nouveau garde-fou ACL), CDP live : `get_version` OK, clic → fenêtre native « Select Folder », ajout de vault complété de bout en bout. | 🟢 corrigé |
|
| 2026-10-02 | BUG-104 | Correction | `desktop/permissions/commands.toml` (nouveau), `desktop/capabilities/default.json`, `desktop/build.rs` | **BUG-104 — commandes Tauri rejetées par l'ACL depuis la page backend** : la fenêtre redirige vers `http://127.0.0.1:<port>`, origine *remote* pour Tauri v2, et aucune commande applicative n'était déclarée → `Command … not allowed by ACL` sur `pick_vault_folder`, `get_wizard_state`, etc. ; les erreurs sont avalées par `desktop.js:invoke()` → bouton du wizard silencieusement inopérant depuis #77. Correctif : manifeste `desktop/permissions/commands.toml` (`allow-app-commands`, 19 commandes) référencé par la capability + `rerun-if-changed=permissions` dans `build.rs`. Vérifié : `cargo test` 25 passed (nouveau garde-fou ACL), CDP live : `get_version` OK, clic → fenêtre native « Select Folder », ajout de vault complété de bout en bout. | 🟢 corrigé |
|
||||||
| 2026-10-02 | BUG-105 | Correction | `desktop/src/jumplist.rs` | **BUG-105 — crash heap (c0000374) à l'ouverture du desktop** : `set_link_title` (#77) passait un `PROPVARIANT VT_LPWSTR` pointant un buffer heap Rust au property store du shell, qui le libère avec l'allocateur Windows → `STATUS_HEAP_CORRUPTION` détectée dans ntdll ≤20 s après le lancement (3/3, Event Log WER, même empreinte de pile). Fonction supprimée : le libellé des entrées vient de `SetDescription`. Vérifié : bisect sur 3 états de build, run 9 min `EXIT=0`, zéro événement WER. | 🟢 corrigé |
|
| 2026-10-02 | BUG-105 | Correction | `desktop/src/jumplist.rs` | **BUG-105 — crash heap (c0000374) à l'ouverture du desktop** : `set_link_title` (#77) passait un `PROPVARIANT VT_LPWSTR` pointant un buffer heap Rust au property store du shell, qui le libère avec l'allocateur Windows → `STATUS_HEAP_CORRUPTION` détectée dans ntdll ≤20 s après le lancement (3/3, Event Log WER, même empreinte de pile). Fonction supprimée : le libellé des entrées vient de `SetDescription`. Vérifié : bisect sur 3 états de build, run 9 min `EXIT=0`, zéro événement WER. | 🟢 corrigé |
|
||||||
|
| 2026-10-02 | BUG-106 | Correction | `backend/main.py`, `tests/test_csp_nonce.py` | **BUG-106 — page blanche sur /docs** : la CSP `#87 T5c` (`script-src` sans `unsafe-inline`, nonce frais par réponse) s'applique aussi aux pages générées par FastAPI (`/docs`, `/redoc`) dont le balisage n'est jamais passé par `inject_csp_nonce` → le navigateur refusait le script inline `SwaggerUIBundle(…)`, la div restait vide. `SecurityHeadersMiddleware` injecte désormais le nonce dans ce HTML avec le helper existant ; le corps est décompressé/recompressé car `SSESafeGZipMiddleware` est plus proche de la route (les réponses arrivent gunzippées). `/redoc` n'a aucun script inline (bundle jsdelivr seul, couvert par `script-src`) — vérifié tel quel. Vérifié : `test_docs_inline_script_nonced`, `test_redoc_served_with_csp`, `test_docs_external_bundle_not_nonced` + 45 passed sur les 3 suites sécurité, ruff/mypy 0. | 🟢 corrigé |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.50.0 | **Dernière mise à jour :** 2026-10-02
|
> **Version :** 2.50.1 | **Dernière mise à jour :** 2026-10-02
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.50.0",
|
"version": "2.50.1",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -80,3 +80,35 @@ def test_nonce_matches_injected_html(client):
|
|||||||
nonce = _nonce_of(resp.headers.get("content-security-policy"))
|
nonce = _nonce_of(resp.headers.get("content-security-policy"))
|
||||||
assert nonce, path
|
assert nonce, path
|
||||||
assert f'nonce="{nonce}"' in resp.text, path
|
assert f'nonce="{nonce}"' in resp.text, path
|
||||||
|
|
||||||
|
|
||||||
|
def test_docs_inline_script_nonced(client):
|
||||||
|
"""BUG-106 : /docs (script inline d'init Swagger) porte le nonce de son
|
||||||
|
en-tête — sinon script-src sans 'unsafe-inline' rend la page blanche."""
|
||||||
|
resp = client.get("/docs")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
nonce = _nonce_of(resp.headers.get("content-security-policy"))
|
||||||
|
assert nonce
|
||||||
|
assert f'nonce="{nonce}"' in resp.text, "/docs : script inline sans nonce"
|
||||||
|
# Le bundle externe (jsdelivr) reste tel quel, couvert par script-src.
|
||||||
|
assert "<script src=" in resp.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_redoc_served_with_csp(client):
|
||||||
|
"""/redoc n'a aucun script inline (bundle externe seul) : il est servi
|
||||||
|
avec sa CSP sans aucune injection nécessaire."""
|
||||||
|
resp = client.get("/redoc")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert _nonce_of(resp.headers.get("content-security-policy"))
|
||||||
|
assert "<script src=" in resp.text
|
||||||
|
assert "<script nonce=" not in resp.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_docs_external_bundle_not_nonced(client):
|
||||||
|
"""L'injection ne touche que les balises <script> exécutables sans src."""
|
||||||
|
from backend.csp import inject_csp_nonce
|
||||||
|
|
||||||
|
html = '<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/x.js"></script><script>init()</script>'
|
||||||
|
out = inject_csp_nonce(html, NONCE)
|
||||||
|
assert out.count(f'nonce="{NONCE}"') == 1
|
||||||
|
assert '<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/x.js">' in out
|
||||||
|
|||||||
Reference in New Issue
Block a user