From d6fa8c7bb148630f7644b22df360bc15a988ecc7 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 2 Oct 2026 22:04:22 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20injecte=20le=20nonce=20CSP=20dans=20les?= =?UTF-8?q?=20pages=20/docs=20et=20/redoc=20g=C3=A9n=C3=A9r=C3=A9es=20par?= =?UTF-8?q?=20FastAPI=20(BUG-106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 +++++++++++++- README.fr.md | 6 +++--- README.md | 6 +++--- VERSION | 2 +- backend/main.py | 26 +++++++++++++++++++++++++- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ISSUES_TODOLIST.md | 2 ++ docs/ROADMAP.md | 2 +- package.json | 2 +- tests/test_csp_nonce.py | 32 ++++++++++++++++++++++++++++++++ 12 files changed, 84 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 392d564..3f9f636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). > **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 ### Ajouté diff --git a/README.fr.md b/README.fr.md index 27ad7d5..24f12ef 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.50.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.50.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 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* diff --git a/README.md b/README.md index b4d747a..8c475c4 100644 --- a/README.md +++ b/README.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.50.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.50.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 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* diff --git a/VERSION b/VERSION index 9e29315..895eb8a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.50.0 +2.50.1 diff --git a/backend/main.py b/backend/main.py index d5b38e4..9cb3cd2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -167,7 +167,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): """Add security headers to all HTTP responses.""" 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 # dans le balisage HTML par les routes (backend.csp.inject_csp_nonce). @@ -200,6 +200,30 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): "form-action '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: # ``immutable``/long max-age made Cloudflare and mobile browsers serve # a stale build for a year (the service worker cache compounded it). diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 68e7d98..9e26270 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.50.0" +version = "2.50.1" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 009a871..7e82196 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.50.0" +version = "2.50.1" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 328777d..ec7f5f5 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.50.0", + "version": "2.50.1", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 0684ff1..330d861 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -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-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-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) @@ -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-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:`, 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-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é | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index fa0d3eb..1ef4632 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # 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 > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/package.json b/package.json index e5bd8f7..1235bf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "main": "patch.js", "directories": { diff --git a/tests/test_csp_nonce.py b/tests/test_csp_nonce.py index 08c2fd8..22c492d 100644 --- a/tests/test_csp_nonce.py +++ b/tests/test_csp_nonce.py @@ -80,3 +80,35 @@ def test_nonce_matches_injected_html(client): nonce = _nonce_of(resp.headers.get("content-security-policy")) assert nonce, 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 "' + out = inject_csp_nonce(html, NONCE) + assert out.count(f'nonce="{NONCE}"') == 1 + assert '