From d70ecd0968bb6166fbbd3e84b4db99bb34098c18 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sat, 26 Sep 2026 22:44:10 -0400 Subject: [PATCH] securite: #87 T5b nonces CSP prets pour bascule (sans changement) --- CHANGELOG.md | 142 ++++++++++++++-------- README.fr.md | 6 +- README.md | 6 +- VERSION | 2 +- backend/csp.py | 35 ++++++ backend/main.py | 47 +++++-- backend/routers/sharing.py | 24 ++-- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ROADMAP.md | 4 +- frontend/js/excalidraw-viewer.js | 2 +- package.json | 2 +- tests/frontend/excalidraw-viewer.test.mjs | 5 +- tests/test_csp_nonce.py | 82 +++++++++++++ 15 files changed, 278 insertions(+), 85 deletions(-) create mode 100644 backend/csp.py create mode 100644 tests/test_csp_nonce.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 633206a..09badf7 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.28.6**. +> [Unreleased](#unreleased). La dernière version livrée est **2.28.7**. --- @@ -14,8 +14,29 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.28.7] — 2026-09-26 + +### Ajouté + +- **#87 (T5b) — nonces CSP prêts pour la bascule (sans changement).** + Nonce frais par réponse dans `script-src` (`backend/csp.py`), injecté + dans les 6 pages HTML servies (dont la nouvelle route + `/excalidraw-editor.html`, utilisée par l'iframe du viewer au lieu de + `/static/`) et la page de partage ; `tests/test_csp_nonce.py` (unicité, + concordance en-tête/HTML). `unsafe-inline` conservé jusqu'en T5c. + +--- + ## [2.28.6] — 2026-09-26 +### Modifié + +- **#87 (T5a) — 16 handlers inline convertis en listeners (CSP inchangée).** + `onclick`/`onerror` de `index.html` et des vues JS (`config`, `plugins`, + `sync`, `viewer`, `auth`) remplacés par `addEventListener` ; suites + frontend vertes (unit, ai, config-mobile, pdf-viewer, mfa-settings, + sidebar-filters). + --- ## [2.28.5] — 2026-09-26 @@ -24,14 +45,6 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.28.4] — 2026-09-26 ---- - -## [2.28.3] — 2026-09-26 - ---- - -## [2.28.2] — 2026-09-26 - ### Ajouté - **#87 (T4) — E2E XSS et serveur E2E piloté.** @@ -41,6 +54,12 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `scripts/e2e-server.ps1` : `start|stop|status|logs` avec progression visible et fichier PID (fini les serveurs orphelins sur le port 2029). +--- + +## [2.28.3] — 2026-09-26 + +### Ajouté + - **#87 (T3) — cookies `Secure` et CORS explicites.** Helper `is_secure_cookies()` centralisé (défaut `false` conservé pour ne pas casser les logins HTTP locaux) + avertissement au démarrage sur bind @@ -48,6 +67,12 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). l'absence de CORS permissif (same-origin par défaut du navigateur) et les en-têtes de durcissement. +--- + +## [2.28.2] — 2026-09-26 + +### Ajouté + - **#87 (T2) — tests de durcissement : concurrence et regex.** `tests/test_hardening_concurrency.py` : créations/mises à jour/`login failures` concurrents sur `users.json` (zéro mise à jour perdue, JSON @@ -73,50 +98,6 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.28.0] — 2026-09-26 ---- - -## [2.27.12] — 2026-09-26 - ---- - -## [2.27.11] — 2026-09-26 - ---- - -## [2.27.10] — 2026-09-26 - ---- - -## [2.27.9] — 2026-09-26 - ---- - -## [2.27.8] — 2026-09-26 - ---- - -## [2.27.7] — 2026-09-26 - ---- - -## [2.27.6] — 2026-09-26 - ---- - -## [2.27.5] — 2026-09-26 - ---- - -## [2.27.4] — 2026-09-26 - ---- - -## [2.27.3] — 2026-09-26 - ---- - -## [2.27.2] — 2026-09-26 - ### Modifié - **#85 (T10) — persistance d'état et clôture de la refonte architecturale.** @@ -129,6 +110,11 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). câblés, rien à coder). Index non persisté : rebuild différentiel #86 suffisant (décision documentée). Fiche `docs/features/archi-refonte-85.md`, #85 sorti du backlog (index roadmap). + +## [2.27.12] — 2026-09-26 + +### Modifié + - **#85 (T9) — extraction realtime + render hors du monolithe `backend/main.py`.** Le stream SSE `/api/events` et le WebSocket `/ws/collab/*` sont servis par `backend/routers/realtime.py`, le pipeline markdown (mistune, wikilinks, @@ -136,30 +122,55 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). couplage différé). `main.py` (4 827 → ~760 lignes) ne contient plus que l'assemblage : lifespan, middlewares, montage des 16 routers, racine `/api`, statique/SPA et cales de compatibilité testées. + +## [2.27.11] — 2026-09-26 + +### Modifié + - **#85 (T8) — extraction vaults/history/conflicts hors du monolithe `backend/main.py`.** 13 routes servies par `backend/routers/vaults.py`, `history.py` et `conflicts.py` ; `VaultInfo`/`BookmarkToggleRequest` dans `schemas.py`, handle watcher partagé dans `backend/watcher_state.py`. `tests/test_api_main.py` importe `humanize_mtime` depuis son module canonique (`services.recent`). + +## [2.27.10] — 2026-09-26 + +### Modifié + - **#85 (T7) — extraction du domaine `config` hors du monolithe `backend/main.py`.** `/api/config`, ai-keys (get/post/delete/test), tool-keys (×3), ai-models, diagnostics et dashboard sont servis par `backend/routers/config.py` (`_FALLBACK_MODELS`, store clés et config déplacés ; `main` réimporte `_load_config` pour son lifespan, les fixtures de tests inchangées). `tests/test_ai_models.py` patch désormais la référence du router. + +## [2.27.9] — 2026-09-26 + +### Modifié + - **#85 (T6c) — extraction media/pdf/export/guide hors du monolithe `backend/main.py`.** file/pdf, exports (html/md-bundle/epub), guide/download, pdf/stream|info, image, media+thumb, attachments (rescan/stats), vault settings (get/post/all) et vault files sont servis par `backend/routers/files_media.py` ; le helper Range partagé vit dans `backend/routers/helpers.py` (tags OpenAPI inchangés, tests statiques frontend `media-viewer`/`image-viewer` réalignés). + +## [2.27.8] — 2026-09-26 + +### Modifié + - **#85 (T6b) — extraction mutations fichiers/dossiers hors du monolithe `backend/main.py`.** `PUT .../save|xlsx/save`, `DELETE/POST/PATCH /api/file`, `POST/PATCH/DELETE /api/directory`, `POST /api/move`, `POST .../batch-upload` sont servis par le nouveau `backend/routers/files_write.py` (effets de bord inchangés : audit, index, SSE, webhooks, plugins, historique) ; 15 modèles dans `schemas.py`. + +## [2.27.7] — 2026-09-26 + +### Modifié + - **#85 (T6a) — extraction lecture fichiers hors du monolithe `backend/main.py`.** `/api/browse/{vault}`, `/api/file/{vault}/raw|download|backlinks` et `GET /api/file/{vault}` (vue rendue tous formats) sont servis par le @@ -168,6 +179,11 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `backend/routers/helpers.py` (partagés avec les tranches suivantes). Correctif au passage : décorateur orphelin `/s/{token}` resté en T3 et double-enregistrement de `/api/conflicts` supprimés. + +## [2.27.6] — 2026-09-26 + +### Modifié + - **#85 (T5) — extraction du domaine `search` hors du monolithe `backend/main.py`.** Les 11 routes (`/api/search`, `/advanced`, `/replace`, `/tags`, `/tree-search`, `/vault/{vault}/paths`, `/suggest`, `/tags/suggest`, @@ -175,22 +191,42 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). par le nouveau `backend/routers/search.py` ; les modèles search dans `schemas.py` et le pool de threads dans `backend/search_executor.py` (même dimensionnement, même cycle de vie) — comportement inchangé. + +## [2.27.5] — 2026-09-26 + +### Modifié + - **#85 (T4) — extraction du domaine `backups` hors du monolithe `backend/main.py`.** Les 9 routes (`/api/file/{vault}/backups|diff|restore`, `/api/backups`, `/delete`, `/purge`, `/content`, `/compress`, `/auto`) sont servies par le nouveau `backend/routers/backups.py` ; `Diff/Restore*` déménagent dans `schemas.py` et le singleton SSE dans `backend/sse.py` (partagé avec `main`) — comportement inchangé, aucun impact utilisateur. + +## [2.27.4] — 2026-09-26 + +### Modifié + - **#85 (T3) — extraction du domaine `sharing` hors du monolithe `backend/main.py`.** `POST /api/share/{vault}`, `GET /api/shares`, `DELETE /api/share/{share_id}` et les pages publiques `/s/{token}`, `/s/{token}/raw`, `/s/{token}/pdf` sont servis par le nouveau `backend/routers/sharing.py` — chemins, réponses, tags OpenAPI et authentification inchangés (aucun impact utilisateur). + +## [2.27.3] — 2026-09-26 + +### Modifié + - **#85 (T2) — extraction du domaine `webhooks` hors du monolithe `backend/main.py`.** Le CRUD `GET/POST/PATCH/DELETE /api/webhooks` (admin) est servi par le nouveau `backend/routers/webhooks.py` — chemins, réponses, tags OpenAPI et authentification inchangés (aucun impact utilisateur). + +## [2.27.2] — 2026-09-26 + +### Modifié + - **#85 (T1) — extraction du domaine `health` hors du monolithe `backend/main.py`.** `GET /api/health` et `GET /api/health/detailed` (admin) sont servis par le nouveau `backend/routers/health.py` (monté dans `main.py`) et le modèle diff --git a/README.fr.md b/README.fr.md index ae27164..c36c132 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.28.6-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.28.7-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.28.6). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.28.7). --- -*Projet : ObsiGate | Version : 2.28.6 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.28.7 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 8847bc4..e6f964e 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.28.6-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.28.7-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.28.6). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.28.7). --- -*Project: ObsiGate | Version: 2.28.6 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.28.7 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 759bcf0..0447f9a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.28.6 +2.28.7 diff --git a/backend/csp.py b/backend/csp.py new file mode 100644 index 0000000..7398430 --- /dev/null +++ b/backend/csp.py @@ -0,0 +1,35 @@ +"""Content-Security-Policy nonces (ROADMAP #87, tranche 5b). + +Chaque réponse HTTP reçoit un nonce frais (``request.state.csp_nonce``) +injecté dans ``script-src``. Les routes servant du HTML avec des scripts +inline (index, popout, admin, editor-poc, excalidraw, page de partage) +l'injectent dans le balisage via :func:`inject_csp_nonce` — mêmes +emplacements, aucun script déplacé. + +Tant que ``'unsafe-inline'`` reste dans la politique (retrait en T5c), +l'injection est inerte : elle prépare la bascule sans changer le +comportement. +""" + +from __future__ import annotations + +import re +import secrets + +# Balises """) +document.querySelector("[data-share-theme]").addEventListener("click",toggleTheme); +document.querySelector("[data-share-md]").addEventListener("click",exportMD); +document.querySelector("[data-share-pdf]").addEventListener("click",function(){{location.href=location.pathname+"/pdf"}}); +""", + request.state.csp_nonce, + ), + ) diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 35b5272..ae728a5 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.28.6" +version = "2.28.7" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 55e3feb..60ec5ed 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.28.6" +version = "2.28.7" 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 a498aa1..6a59258 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.28.6", + "version": "2.28.7", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ffb6032..831667e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.28.6 | **Dernière mise à jour :** 2026-09-26 +> **Version :** 2.28.7 | **Dernière mise à jour :** 2026-09-26 > **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)** @@ -72,7 +72,7 @@ - **Sous-tâches :** - [ ] Jobs CI sécurité **bloquants** (bandit/semgrep/trivy, audits pip/npm) + tests E2E XSS (page de partage + lecteur markdown) — **T4 livrée :** `tests/e2e/xss.spec.js` (BUG-021/022, 2/2 vert) + `scripts/e2e-server.ps1` (cycle de vie serveur E2E avec progression `start|stop|status|logs`) + validation locale projet `chromium-desktop` : **108/108 verts** (obsigate 44, split 37, viewers 24, xss/header 3), mobiles ciblés 10/10 - [ ] Tests de concurrence (`users.json`), fuzzing de timing regex, couverture des composants critiques ; intégrer au CI les 5 suites frontend hors CI (`upload`, `pretty`, `media-viewer`, `mfa-settings`, `config-ai-keys`) — **T2 livrée (v2.28.2) :** `tests/test_hardening_concurrency.py` (users.json concurrent + budget temps regex) ; 5 suites au CI (T1) - - [ ] Finir BUG-034 (migration CSP **nonce**, conversion des handlers inline), `Secure` cookies à `true` par défaut, politique CORS same-origin explicite ; confirmer la rotation de la clé DeepSeek (BUG-006, clé dans l'historique Git) — **T3 livrée (v2.28.3) :** helper `is_secure_cookies()` centralisé (défaut `false` conservé : compatibilité HTTP local), avertissement au démarrage sur bind non-loopback sans `Secure`, absence CORS attestée par test (`tests/test_security_headers.py`) ; bascule du défaut reportée (casserait les logins localhost) + - [ ] Finir BUG-034 (migration CSP **nonce**, conversion des handlers inline), `Secure` cookies à `true` par défaut, politique CORS same-origin explicite ; confirmer la rotation de la clé DeepSeek (BUG-006, clé dans l'historique Git) — **T3 livrée (v2.28.3)** (helper + avertissement + CORS attesté) ; **T5a livrée (v2.28.6)** (16 handlers inline → listeners, CSP inchangée) ; **T5b livrée :** nonce frais par réponse (`backend/csp.py`, `script-src`), injection dans les 6 pages HTML (dont nouvelle route `/excalidraw-editor.html`), `unsafe-inline` conservé (inerte) - [ ] Revue périodique des dépendances ; documentation utilisateur FR/EN synchronisée ; contrôle automatisé de la conformité au DoD --- diff --git a/frontend/js/excalidraw-viewer.js b/frontend/js/excalidraw-viewer.js index 7eff43f..4ba525a 100644 --- a/frontend/js/excalidraw-viewer.js +++ b/frontend/js/excalidraw-viewer.js @@ -35,7 +35,7 @@ export function renderExcalidraw(container, data, vaultName, filePath, opts = {} // Build the iframe const iframe = document.createElement('iframe'); iframe.id = editorId; - iframe.src = '/static/excalidraw-editor.html?v=' + Date.now(); + iframe.src = '/excalidraw-editor.html?v=' + Date.now(); iframe.sandbox.add('allow-scripts'); iframe.sandbox.add('allow-same-origin'); // Let the editor's own Fullscreen button work (native Fullscreen API inside diff --git a/package.json b/package.json index 4774019..b5f8fdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.28.6", + "version": "2.28.7", "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/frontend/excalidraw-viewer.test.mjs b/tests/frontend/excalidraw-viewer.test.mjs index 4adedee..0e56c75 100644 --- a/tests/frontend/excalidraw-viewer.test.mjs +++ b/tests/frontend/excalidraw-viewer.test.mjs @@ -100,7 +100,7 @@ await test("module exports renderExcalidraw + helpers", () => { assert.equal(typeof destroyExcalidrawEditor, "function"); }); -await test("renderExcalidraw creates an iframe with sandbox + static src", () => { +await test("renderExcalidraw creates an iframe with sandbox + routed src", () => { const container = document.getElementById("content-area"); const data = { is_excalidraw: true, @@ -111,7 +111,8 @@ await test("renderExcalidraw creates an iframe with sandbox + static src", () => renderExcalidraw(container, data, "TestVault", "diagram.excalidraw"); const iframe = container.querySelector("iframe"); assert.ok(iframe, "iframe should be created"); - assert.ok(iframe.src.includes("/static/excalidraw-editor.html"), `src: ${iframe.src}`); + assert.ok(iframe.src.includes("/excalidraw-editor.html"), `src: ${iframe.src}`); + assert.ok(!iframe.src.includes("/static/excalidraw-editor.html"), `route avec nonce CSP: ${iframe.src}`); assert.ok(iframe.sandbox.contains("allow-scripts"), "sandbox allow-scripts"); assert.ok(iframe.sandbox.contains("allow-same-origin"), "sandbox allow-same-origin"); assert.match(iframe.style.cssText, /100%/); diff --git a/tests/test_csp_nonce.py b/tests/test_csp_nonce.py new file mode 100644 index 0000000..08c2fd8 --- /dev/null +++ b/tests/test_csp_nonce.py @@ -0,0 +1,82 @@ +"""Tests — nonces CSP (ROADMAP #87 T5b). + +- `inject_csp_nonce` ne touche que les scripts inline exécutables + (`" + '' + '' + '' + '' + '' + '' + ) + out = inject_csp_nonce(html, NONCE) + assert out.count(f'nonce="{NONCE}"') == 3 + assert '