Files
stupid-simple-network-inven…/SECURITY_REVIEW_PHASE2.md
T
olivier 88cf6458d0 Initial commit — Stupid Simple Network Inventory
Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker.
Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 09:19:19 +02:00

12 KiB

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:

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:

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.