Files
stupid-simple-network-inven…/SECURITY_FIXES_APPLIED_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

10 KiB

Security Fixes Applied — Phase 2

Date: 2026-05-06

SEC-FIX-001 — Bootstrap admin et rattrapage des bases existantes

Problème résiduel (après phase 1)

La migration _migrate_users_must_change_password() ajoutait la colonne avec DEFAULT 0. Un compte admin existant avec le mot de passe par défaut "admin" n'était donc pas forcé à changer.

Corrections appliquées

backend/main.py

  • Nouvelle migration _migrate_users_token_version() — ajout de token_version INTEGER NOT NULL DEFAULT 1 si absente.
  • Nouvelle migration _migrate_force_admin_password_change() : vérifie si l'admin a must_change_password=0 et si son hash correspond au mot de passe "admin". Si oui, pose must_change_password=1. Idempotent, s'exécute à chaque démarrage.
  • _migrate_users() : mise à jour pour inclure token_version dans le CREATE TABLE initial.
  • Ordre de démarrage mis à jour : _migrate_users_token_version() puis _migrate_force_admin_password_change() avant _migrate_users().

backend/tests/conftest.py (nouveau)

  • Configure DATABASE_URL vers une base temporaire avant tout import applicatif.
  • SECRET_KEY fixé pour les tests.

backend/tests/test_auth.py (nouveau)

  • Tests SEC-FIX-001 : base vide (must_change=1), CRUD bloqué avant changement, CRUD autorisé après, rattrapage admin existant, admin avec mot de passe personnalisé non touché, INITIAL_ADMIN_PASSWORD.
  • Tests SEC-FIX-004 : ancien token rejeté après changement, nouveau token valide, backward compat token sans ver, token avec mauvaise version.
  • Tests SEC-FIX-003 : validation mot de passe (trop court, sans chiffre, sans lettre, valide), username (invalide, trop long, valide), mauvais mot de passe courant.
  • Tests SEC-FIX-002 : rate limit IP (429), rate limit username (429), reset après login réussi.

backend/requirements-test.txt (nouveau)

  • pytest>=7.4, httpx>=0.25.

SEC-FIX-004 — Invalidation de session après changement de mot de passe

Problème

JWT valable 7 jours, pas d'invalidation après changement de mot de passe.

Corrections appliquées

backend/models.py

  • Colonne token_version = Column(Integer, nullable=False, default=1, server_default="1") ajoutée à User.

backend/routers/auth.py

  • TOKEN_EXPIRE_HOURS = 24 (réduit de 7 jours à 24 heures).
  • create_token(username, version) : payload inclut { ver: version }.
  • get_current_user : vérifie payload["ver"] == user.token_version. Rejette avec 401 si différent. Rétrocompatibilité : ver absent dans le payload est traité comme ver=1.
  • update_account : incrémente current_user.token_version lors d'un changement de mot de passe.
  • login : transmet user.token_version au create_token.

SEC-FIX-005 — CORS configurable

Problème

allow_origins=["*"] hardcodé.

Corrections appliquées

backend/main.py

  • Lecture de ALLOWED_ORIGINS env var (défaut "*" pour rétrocompatibilité).
  • "" désactive le middleware CORS (même origine via proxy).
  • Valeur CSV : liste d'origines explicites.

.env.example (nouveau)

  • Documentation de ALLOWED_ORIGINS avec exemples.

SEC-FIX-010 — Sécurisation du secret JWT

Problème

secret_key.txt créé sans permissions restrictives. Pas de .gitignore.

Corrections appliquées

backend/routers/auth.py

  • Création du fichier secret via os.open(..., os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) pour imposer des permissions 0600 dès la création (owner read/write uniquement).

.gitignore (nouveau)

  • Ignore db_data/, *.db, .env, __pycache__/, node_modules/, etc.

SEC-FIX-011 — Durcissement des conteneurs

Problème

Conteneurs lancés en root sans contraintes de capabilities ni healthchecks.

Corrections appliquées (Phase 2 initiale)

frontend/Dockerfile

  • npm installnpm ci (build reproductible, respecte le lockfile).
  • Image de base finale : nginxinc/nginx-unprivileged:alpine — nginx s'exécute entièrement en UID 101 sans nécessiter CAP_CHOWN ni CAP_SETUID. Compatible avec cap_drop: ALL.
  • EXPOSE 8080 (port non-privilégié, correspondant à la config nginx).

frontend/nginx.conf

  • listen 80listen 8080 (port non-privilégié, ne nécessite pas CAP_NET_BIND_SERVICE).

docker-compose.yml

  • frontend : cap_drop: ALL, security_opt: no-new-privileges:true, port 8080:8080, healthcheck wget, depends_on: backend: condition: service_healthy.
  • Suppression de la section volumes: db_data: (bind mount, pas de volume nommé).

Approches abandonnées (Phase 2 initiale)

  • gosu + appuser pour le backend : cap_drop: ALL supprime CAP_SETUIDgosu ne peut pas changer d'utilisateur depuis le processus uvicorn après démarrage.

Correction complémentaire — backend non-root via user: Compose

Problème résiduel : le backend tournait en UID 0 (root) faute de mécanisme interne pour changer d'utilisateur. La capability DAC_OVERRIDE avait été ajoutée comme contournement (accès en écriture au bind-mount), mais les fichiers db_data/ étaient créés root:root sur l'hôte.

Solution : la directive user: de Docker Compose démarre le processus directement sous l'UID/GID cible, sans nécessiter CAP_SETUID ni gosu.

docker-compose.yml

  • backend : ajout de user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}".
  • cap_add : suppression de DAC_OVERRIDE (inutile — le processus est propriétaire du bind-mount).
  • cap_add conserve NET_RAW (ICMP ping).
  • no-new-privileges reste omis : ping utilise les file capabilities (cap_net_raw=ep) ; no-new-privileges supprime le bit effective du fichier, empêchant le sous-processus d'acquérir CAP_NET_RAW dans son ensemble effectif même si le parent le détient dans son ensemble permis.

.env.example

  • DOCKER_UID / DOCKER_GID documentés avec procédure id -u && id -g et instruction mkdir -p db_data.

README.md

  • Quick start : docker compose --env-file .env up --build -d.
  • Configuration : APP_UID/APP_GID remplacés par DOCKER_UID/DOCKER_GID.
  • Container hardening : table mise à jour (DOCKER_UID:DOCKER_GID), note no-new-privileges précisée.

SEC-FIX-018 — Documentation

Corrections appliquées

README.md (nouveau)

  • Quick start, configuration, sécurité (HTTPS, rotation de clé, durcissement conteneurs), persistance des données, développement.

.env.example (nouveau)

  • SECRET_KEY, INITIAL_ADMIN_PASSWORD, ALLOWED_ORIGINS, APP_UID/APP_GID documentés avec exemples.

docs/backend.md

  • Routeurs protégés : get_current_userrequire_password_changed.
  • Contrat login : ajout must_change_password.
  • Contrat account : codes d'erreur normalisés.
  • Modèle User : ajout must_change_password, token_version.
  • Section Auth : mise à jour expiry, payload, invalidation, rate limiting.
  • Section Migrations : ordre complet, description _migrate_force_admin_password_change.

docs/architecture.md

  • Section Auth : schéma mis à jour (mustChangePassword, AccountModal :forced).
  • Section Docker Compose : non-root, cap_drop, suppression volume nommé.

CLAUDE.md

  • Section Authentification : 24h, token_version, must_change_password, guard App.vue, INITIAL_ADMIN_PASSWORD.
  • Section Migrations : liste complète des 7 migrations.

AGENTS.md

  • Section Authentication : réécriture complète.
  • Data Model : User mis à jour.

Fichiers modifiés

Fichier Fixes
backend/database.py Support DATABASE_URL pour tests
backend/models.py SEC-FIX-004 (token_version)
backend/main.py SEC-FIX-001 (rattrapage), SEC-FIX-004 (migration), SEC-FIX-005 (CORS)
backend/routers/auth.py SEC-FIX-004 (versioning), SEC-FIX-010 (0600), expiry 24h
backend/Dockerfile SEC-FIX-011 (cap_drop, DAC_OVERRIDE)
backend/requirements-test.txt Tests (nouveau)
backend/tests/__init__.py Tests (nouveau)
backend/tests/conftest.py Tests (nouveau)
backend/tests/test_auth.py Tests SEC-FIX-001, 002, 003, 004 (nouveau)
frontend/Dockerfile SEC-FIX-011 (nginxinc/nginx-unprivileged, npm ci)
frontend/nginx.conf SEC-FIX-011 (port 8080)
docker-compose.yml SEC-FIX-011 (cap_drop, healthchecks)
.gitignore SEC-FIX-010 (nouveau)
.env.example SEC-FIX-005, SEC-FIX-010, SEC-FIX-018 (nouveau)
README.md SEC-FIX-018 (nouveau)
docs/backend.md SEC-FIX-018
docs/architecture.md SEC-FIX-018
CLAUDE.md SEC-FIX-018
AGENTS.md SEC-FIX-018

Risques couverts

Risque Avant Après
Bootstrap admin/admin sur instance existante Non corrigé _migrate_force_admin_password_change() force le changement au prochain démarrage
JWT valable 7 jours après compromission Oui 24h + invalidation immédiate par token_version
CORS permissif Hardcodé * Configurable via ALLOWED_ORIGINS, défaut * rétrocompatible
Secret JWT lisible (permissions) Mode par défaut (0644 ou plus) 0600 dès la création
Secrets dans le dépôt Pas de .gitignore db_data/, .env, *.db ignorés
Process frontend root Oui (nginx master + workers en root) nginxinc/nginx-unprivileged — tout le process tree en UID 101
Process backend root Oui user: DOCKER_UID:DOCKER_GID — process = utilisateur hôte, fichiers db_data/ non root-owned
Capabilities superflues Aucun cap_drop cap_drop: ALL sur les deux conteneurs ; backend ajoute NET_RAW uniquement
DAC_OVERRIDE exposée Oui (contournement root) Supprimée — inutile avec non-root propriétaire du bind-mount
Pas de healthchecks Aucun Ajoutés sur les deux services
Build frontend non reproductible npm install npm ci

Limitations restantes

  • JWT stocké en localStorage (pas de cookie HttpOnly) — mitigation : expiry 24h + token versioning.
  • Rate limiting backend en mémoire (perdu au redémarrage, ne se partage pas entre workers) — mitigation : Nginx limit_req couvre le cas multi-worker.
  • no-new-privileges absent du backend : no-new-privileges supprime le bit effective des file capabilities ; le sous-processus ping ne peut pas acquérir CAP_NET_RAW dans son ensemble effectif même si le parent le détient en permis. Mitigation : le processus tourne désormais sous un UID non-privilégié.
  • Politique de mot de passe minimale (password1 passe) — pas de liste noire de mots de passe communs.
  • Pas de logs structurés d'audit (SEC-FIX-012 non traité dans cette phase).