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>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user