# Revue securite Phase 2 Date: 2026-05-06 Base: `SECURITY_REVIEW_AFTER_FIXES.md`, `SECURITY_FIXES_APPLIED_PHASE2.md`, code actuel. Mode: revue statique locale + builds + controles cibles en conteneur. Aucun fichier applicatif n'a ete modifie. ## Synthese Les corrections Phase 2 principales sont bien presentes dans le code actuel: rattrapage admin utilisant encore `admin`, `token_version` JWT, expiration 24h, CORS configurable, creation des nouvelles cles JWT en `0600`, build frontend reproductible via `npm ci`, conteneurs Compose avec privileges reduits, et documentation globalement enrichie. Deux points importants restent a corriger: - La suite de tests Phase 2 n'est pas reproductible telle que declaree: `backend/requirements-test.txt` laisse installer `httpx>=0.28`, incompatible avec le `TestClient` de Starlette utilise par FastAPI 0.104.1. - SEC-FIX-010 ne corrige que les nouvelles creations de `secret_key.txt`; un fichier existant cree avant Phase 2 avec des permissions trop larges est relu tel quel, sans `chmod 0600`. Verification executee: - `npm run build` dans `frontend`: OK. - `docker compose build`: OK. - Controles conteneur: backend sous UID/GID `1000:1000`, frontend sous UID `101`, ping OK avec `cap_drop: ALL` + `cap_add: NET_RAW`, secret auto-genere en `0600`, bootstrap admin OK, rattrapage admin existant OK. - `pytest -q tests` avec `requirements-test.txt` actuel: ECHEC, incompatibilite `httpx`. - `pytest -q tests` avec `pytest>=7.4` et `httpx<0.28`: OK, 21 tests passes. ## Verification des corrections Phase 2 ### SEC-FIX-001 - Bootstrap admin et rattrapage des bases existantes Statut: corrige Preuves: - `backend/main.py:65-77` ajoute `must_change_password` aux bases existantes. - `backend/main.py:80-92` ajoute `token_version`. - `backend/main.py:95-111` force `must_change_password=1` si `admin` utilise encore le mot de passe bootstrap `admin`. - `backend/main.py:114-146` cree une base neuve avec `admin/admin` + `must_change_password=1`, ou avec `INITIAL_ADMIN_PASSWORD` + `must_change_password=0`. - `backend/main.py:179-183` protege les routeurs metier via `require_password_changed`. - Controle conteneur: base neuve -> `admin 1 1`; ancienne base admin/admin avec `must_change_password=0` -> `must_change_password=1`; admin avec mot de passe personnalise -> non modifie. Point de vigilance: - `SECURITY_FIXES_APPLIED_PHASE2.md` dit que `_migrate_force_admin_password_change()` s'execute avant `_migrate_users()`. C'est vrai dans le code (`backend/main.py:149-155`) et correct pour les bases existantes. Pour une base neuve, le rattrapage ne s'applique pas car la table n'existe pas encore; le cas est couvert par `_migrate_users()`. ### SEC-FIX-004 - Invalidation de session apres changement de mot de passe Statut: corrige Preuves: - `backend/models.py:13` ajoute `User.token_version`. - `backend/routers/auth.py:44` reduit l'expiration JWT a 24 heures. - `backend/routers/auth.py:84-90` inclut `ver` dans le JWT. - `backend/routers/auth.py:93-107` rejette un token dont `ver` differe de `user.token_version`. - `backend/routers/auth.py:175-180` incremente `token_version` lors d'un changement de mot de passe. - Les tests d'invalidation passent quand la dependance `httpx` est pinnee correctement. Limite restante: - Le JWT reste stocke dans `localStorage` (`frontend/src/auth.js:3-17`) et lu par JavaScript (`frontend/src/api.js:6-9`). Phase 2 reduit la duree et invalide les anciennes sessions, mais ne supprime pas le risque de vol de token en cas de XSS. ### SEC-FIX-005 - CORS configurable Statut: corrige avec risque residuel de configuration Preuves: - `backend/main.py:163-177` lit `ALLOWED_ORIGINS`, supporte `*`, chaine vide, ou CSV d'origines. - `.env.example:25-35` documente la variable. Risque residuel: - Le defaut reste permissif: `ALLOWED_ORIGINS=*` dans `.env.example:35` et `docker-compose.yml:18`. C'est retrocompatible, mais une installation exposee directement garde CORS ouvert si l'operateur ne change rien. ### SEC-FIX-010 - Secret JWT Statut: partiellement corrige Preuves: - `backend/routers/auth.py:31-38` cree les nouveaux secrets via `os.open(..., 0o600)`. - Controle conteneur: secret auto-genere avec mode `0o600`. - `.gitignore:1-8` ignore `db_data/`, `*.db`, `.env`. Risque restant: - `backend/routers/auth.py:27-28` lit un fichier existant sans verifier ni corriger ses permissions. Une installation ayant deja cree `db_data/secret_key.txt` avant Phase 2 avec un mode trop large conserve ce mode apres mise a jour. Recommandation: - Sur chargement du fichier, appliquer ou au minimum verifier `chmod 0600`. - Documenter une remediation explicite: `chmod 600 db_data/secret_key.txt`. ### SEC-FIX-011 - Durcissement conteneurs Statut: corrige pour le chemin Docker Compose fourni Preuves: - `docker-compose.yml:4-8` lance le backend sous `DOCKER_UID:DOCKER_GID`, avec `cap_drop: ALL` et `cap_add: NET_RAW` uniquement. - `docker-compose.yml:35-41` ajoute un healthcheck backend. - `docker-compose.yml:45-62` lance le frontend sur `8080`, avec `cap_drop: ALL`, `no-new-privileges` et healthcheck. - `frontend/Dockerfile:4` utilise `npm ci`. - `frontend/Dockerfile:8-12` utilise `nginxinc/nginx-unprivileged:alpine`. - `frontend/nginx.conf:2` ecoute sur `8080`. - Controle conteneur: backend `uid=1000 gid=1000`; frontend `uid=101(nginx)`; ping `127.0.0.1` OK avec la configuration de capabilities. Limites: - `backend/Dockerfile` ne definit pas `USER`; le non-root depend de Compose. Un lancement direct de l'image backend sans `--user` tournera en root. - Le backend n'a pas `no-new-privileges`, par choix documente pour conserver le fonctionnement de `ping`. ## Regressions et nouveaux risques ### R1 - Tests Phase 2 non reproductibles avec `requirements-test.txt` Severite: moyenne `backend/requirements-test.txt:1-2` contient: ```text pytest>=7.4 httpx>=0.25 ``` Dans le conteneur backend actuel, cela installe un `httpx` recent. Resultat: tous les tests utilisant `TestClient(app)` echouent avec: ```text TypeError: Client.__init__() got an unexpected keyword argument 'app' ``` Cause: incompatibilite entre Starlette/FastAPI du projet et `httpx>=0.28`, qui a retire le raccourci `app=`. Verification: - Avec `requirements-test.txt` actuel: 1 failed, 20 errors. - Avec `httpx<0.28`: 21 passed. Correction recommandee: - Pinner `httpx<0.28` dans `backend/requirements-test.txt`, ou mettre a jour FastAPI/Starlette de maniere controlee. ### R2 - Option Docker secret probablement fragile avec backend non-root Severite: moyenne `docker-compose.yml:31-34` documente une option de secret montee vers `/app/data/secret_key.txt` avec `mode: 0400`, pendant que le backend tourne sous `DOCKER_UID:DOCKER_GID` (`docker-compose.yml:4`). Selon le comportement exact de Docker Compose et du proprietaire du fichier source, un secret root-owned en `0400` peut devenir illisible par le backend non-root au demarrage. Correction recommandee: - Tester explicitement cette variante. - Si elle est conservee, specifier `uid`/`gid` compatibles quand Compose le permet, ou supporter une variable `SECRET_KEY_FILE` pointant vers un chemin de secret lisible par l'utilisateur applicatif. ### R3 - Nouvelles cles protegees, anciennes cles non remediƩes Severite: moyenne Voir SEC-FIX-010. Le risque est particulierement important pour les installations deja deployees avant Phase 2, c'est-a-dire precisement le cas traite par les migrations de rattrapage admin. ## Incoherences documentation / code - `docs/extending.md:70-71` recommande encore `dependencies=[Depends(get_current_user)]` pour un nouveau routeur. Le code actuel doit utiliser `require_password_changed` pour les routeurs metier. - `docs/backend.md:5-12` dit "four router groups" mais l'extrait montre `vlans`, `devices`, `discovery` et omet `links`, pourtant enregistre dans `backend/main.py:182`. - `docs/frontend.md:48-55` documente `linksApi`, mais `frontend/src/api.js` n'exporte pas `linksApi`. - `docs/frontend.md:31` documente `setAuth(token, username)` alors que le code actuel attend `setAuth(token, username, mustChange)` (`frontend/src/auth.js:11`). - `docs/frontend.md:114-121` decrit un guard `v-else` direct et des callbacks sans `mustChangePassword`; le code actuel a la branche forcee `AccountModal` (`frontend/src/App.vue:3-11`) et propage `mustChangePassword` (`frontend/src/App.vue:145-153`). - `docs/architecture.md:54-57` dit encore que le stage final frontend est `nginx:alpine`; le code utilise `nginxinc/nginx-unprivileged:alpine` (`frontend/Dockerfile:8`). - `docs/architecture.md:62-67` liste une sequence de demarrage backend obsolete et omet les migrations `_migrate_users_must_change_password`, `_migrate_users_token_version`, `_migrate_force_admin_password_change`. - `AGENTS.md:207` parle d'un "Docker volume `db_data`"; le Compose actuel utilise un bind mount `./db_data:/app/data` (`docker-compose.yml:10-11`). - `.env.example:5` contient un caractere corrompu dans le commentaire de section. Impact faible, mais a nettoyer. - `SECURITY_FIXES_APPLIED_PHASE2.md` garde des traces de l'approche abandonnee (`APP_UID`/`APP_GID`, `DAC_OVERRIDE`, mentions backend Dockerfile) alors que le code final utilise `DOCKER_UID`/`DOCKER_GID` via Compose et a supprime `DAC_OVERRIDE`. ## Points herites confirmes ouverts Ces points etaient deja signales dans `SECURITY_REVIEW_AFTER_FIXES.md` et restent ouverts dans le code actuel: | ID | Etat actuel | |---|---| | SEC-FIX-006 | Discovery: `PingRequest.ips` non borne (`backend/routers/discovery.py:72-85`), DNS libre (`backend/routers/discovery.py:52-60`), limite par reseau mais pas limite totale multi-target (`backend/routers/discovery.py:93-107`). | | SEC-FIX-007 | Validation metier faible: champs libres dans `devices.py`, `vlans.py`, `links.py`; URL/IP/CIDR/couleur/type non contraints cote serveur. Les `rel="noreferrer noopener"` sont en revanche corriges dans les liens frontend. | | SEC-FIX-008 | En-tetes HTTP de securite absents dans `frontend/nginx.conf`: pas de CSP, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, ni politique frame. | | SEC-FIX-009 | Le Compose expose encore `8080:8080` sur toutes les interfaces par defaut (`docker-compose.yml:45-46`). README documente HTTPS et bind loopback, mais ce n'est pas le defaut. | | SEC-FIX-012 | Pas de logs d'audit structures dans les routeurs backend. | | SEC-FIX-013 | SQLite foreign keys non forcees par connexion: pas d'event SQLAlchemy `PRAGMA foreign_keys=ON` dans `backend/database.py`. | | SEC-FIX-014 | `cytoscape` a ete supprime de `frontend/package.json`, mais reste dans `frontend/package-lock.json:12` et dans le `node_modules` local comme dependance extraneous. Le lockfile doit etre regenere, et aucun audit dependances automatise n'est documente/integre. | | SEC-FIX-015 | Import JSON frontend sans limite de taille ni schema, erreurs ignorees par `.catch(() => {})` (`frontend/src/App.vue:190-207`). | | SEC-FIX-016 | `v-html` reste present pour les icones d'onglet (`frontend/src/App.vue:30`). Les valeurs sont hardcodees localement (`frontend/src/App.vue:139-143`), donc risque XSS faible aujourd'hui, mais surface inutile. | ## Conclusion Phase 2 corrige effectivement les risques prioritaires annonces autour du bootstrap admin, de l'invalidation JWT, du CORS configurable, du secret JWT pour nouvelles installations et du durcissement Compose. Avant de considerer Phase 2 comme stabilisee, corriger en priorite: 1. `backend/requirements-test.txt`: pinner `httpx<0.28` ou mettre a jour FastAPI/Starlette. 2. Remediation des permissions des secrets existants (`chmod 0600` au chargement ou migration). 3. Clarification/test de l'option Docker secret avec backend non-root. 4. Mise a jour des incoherences docs/code listees ci-dessus.