# Security Fixes Applied — Phase 3 Date: 2026-05-06 ## SEC-FIX-006 — Validation des entrées discovery ### Problème - `dns_server` acceptait n'importe quelle chaîne (risque d'injection dans le résolveur DNS) - `/api/discovery/ping` acceptait n'importe quelle chaîne comme IP (transmise au sous-processus ping) - Plusieurs targets pouvaient contourner le cap de 1024 : 5 × /22 = 5110 hôtes ### Corrections appliquées **`backend/routers/discovery.py`** - `ScanRequest.dns_server` : `field_validator` qui appelle `ipaddress.ip_address(v)` — rejette toute valeur non-IP (422) - `PingRequest.ips` : `field_validator` qui valide chaque IP — rejette toute entrée malformée avant d'appeler le sous-processus (422) - `MAX_HOSTS_TOTAL = 4096` : cap global sur la somme des hôtes de tous les targets — rejette si dépassé (400) --- ## SEC-FIX-007 — Validation Pydantic métier ### Problème Aucun validator sur les schémas métier : valeurs hors-domaine, CIDR invalides, couleurs arbitraires, URLs arbitraires, IPs invalides, types inconnus acceptés silencieusement. ### Corrections appliquées **`backend/routers/vlans.py`** - `vlan_id` : 1–4094 (norme 802.1Q) - `name` : non vide, max 100 caractères (strip) - `cidr` : `ipaddress.ip_network(strict=False)` si non vide - `color` : regex `^#[0-9a-fA-F]{6}$` **`backend/routers/devices.py`** - `name` : non vide, max 100 caractères (strip) - `description` : max 500 caractères - `type` : enum des 18 types valides - `virt_type` : enum `{null, baremetal, lxc, qemu}` - `url` : `urlparse` — schéma `http`/`https` + netloc non vide - `InterfaceCreate.name` : non vide, max 50 caractères - `InterfaceCreate.ip_address` : `ipaddress.ip_address(v)` si non vide Toutes les erreurs de validation retournent 422 (comportement standard FastAPI/Pydantic). --- ## SEC-FIX-008 — En-têtes HTTP sécurité Nginx ### Problème Aucun en-tête de sécurité HTTP : pas de CSP, pas de protection contre le MIME sniffing ou le framing. ### Corrections appliquées **`frontend/nginx.conf`** ``` X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=() Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; ``` Tous les en-têtes utilisent le flag `always` pour s'appliquer aussi aux réponses d'erreur. --- ## SEC-FIX-009 — Documentation TLS/HTTPS ### Statut : CORRIGÉ EN PHASE 2/3 Documentation ajoutée au `README.md` lors de la session précédente : - Section HTTPS avec exemple nginx reverse-proxy complet - `docker-compose.override.yml` pour limiter l'exposition au loopback --- ## SEC-FIX-012 — Logs d'audit structurés ### Problème Aucune trace des événements d'authentification : connexions, échecs, changements de mot de passe. ### Corrections appliquées **`backend/routers/auth.py`** - Logger stdlib `logging.getLogger("audit")` — aucune dépendance supplémentaire - Helper `_log_audit(event, **kw)` : émet une ligne JSON `{"event": ..., "ts": ..., ...}` - Événements loggés : - `auth.login.success` — username, ip - `auth.login.failure` — username, ip - `auth.login.rate_limited` — ip, username (si disponible), reason (`ip` | `username`) - `auth.token_rejected` — username, reason (`user_not_found` | `version_mismatch`) - `auth.account.password_changed` — username - `auth.account.username_changed` — old_username, new_username - `auth.account.bad_password` — username Activation dans Docker via la config de logging uvicorn (stdout par défaut). --- ## SEC-FIX-013 — PRAGMA foreign_keys=ON ### Problème SQLite désactive les contraintes de clés étrangères par défaut. Les suppressions en cascade ne sont pas enforced. ### Corrections appliquées **`backend/database.py`** ```python @event.listens_for(Engine, "connect") def _set_sqlite_pragma(dbapi_conn, _record): if isinstance(dbapi_conn, sqlite3.Connection): cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() ``` Le listener s'exécute sur chaque nouvelle connexion SQLite, y compris les connexions de test. --- ## SEC-FIX-014 — Dépendance `cytoscape` non utilisée ### Statut : CORRIGÉ EN PHASE 2 `cytoscape` supprimé de `frontend/package.json` lors de la session précédente. --- ## SEC-FIX-015 — Import JSON sans limite de taille ni validation de schéma ### Problème Un fichier JSON de plusieurs gigaoctets pouvait être chargé en mémoire. Aucune validation du schéma avant traitement. ### Corrections appliquées **`frontend/src/App.vue`** - Vérification `file.size > 5 * 1024 * 1024` avant `file.text()` — rejette avec message localisé - Validation de schéma : - Le JSON doit être un objet (pas un tableau, pas null) - `vlans` et `devices`, si présents, doivent être des tableaux **`frontend/src/i18n.js`** - Clé `importTooLarge` ajoutée (fr, en, es) --- ## SEC-FIX-016 — `v-html` dans App.vue ### Problème `v-html` utilisé pour injecter des entités HTML (`■`, `◆`, `▣`) dans les boutons de navigation. Risque XSS si la source venait à être dynamique. ### Corrections appliquées **`frontend/src/App.vue`** - Entités HTML remplacées par leurs équivalents Unicode directs (`■`, `◆`, `▣`) - `v-html="tab.icon"` remplacé par `{{ tab.icon }}` (interpolation texte — échappe automatiquement) --- ## SEC-FIX-017 — Nettoyage du code orphelin Links (backend) ### Problème La vue Liens avait été retirée du frontend en phase 2/3, mais le backend conservait : - `backend/routers/links.py` — router toujours enregistré dans main.py - `class Link` dans `models.py` — ORM orphelin - Référence explicite à `models.Link` dans `delete_device` (devices.py) - La table `links` en base — avec FK vers `devices`, ce qui bloquerait les suppressions d'équipements avec `PRAGMA foreign_keys=ON` ### Corrections appliquées **`backend/routers/links.py`** — Supprimé **`backend/models.py`** — Classe `Link` supprimée **`backend/routers/devices.py`** — Suppression de la requête `db.query(models.Link).filter(...).delete()` dans `delete_device` **`backend/main.py`** - Import `links` retiré - `app.include_router(links.router, ...)` retiré - Nouvelle migration `_migrate_drop_links_table()` : DROP TABLE links si elle existe (avec `PRAGMA foreign_keys=OFF/ON` pour éviter les erreurs FK pendant la migration) --- ## Fichiers modifiés | Fichier | Fix(es) | |---------|---------| | `backend/database.py` | SEC-FIX-013 | | `backend/models.py` | SEC-FIX-017 | | `backend/main.py` | SEC-FIX-017 | | `backend/routers/auth.py` | SEC-FIX-012 | | `backend/routers/vlans.py` | SEC-FIX-007 | | `backend/routers/devices.py` | SEC-FIX-007, SEC-FIX-017 | | `backend/routers/discovery.py` | SEC-FIX-006 | | `backend/routers/links.py` | SEC-FIX-017 (supprimé) | | `backend/tests/test_validation.py` | Tests SEC-FIX-006, 007, 013, 017 (nouveau) | | `frontend/nginx.conf` | SEC-FIX-008 | | `frontend/src/App.vue` | SEC-FIX-015, SEC-FIX-016 | | `frontend/src/i18n.js` | SEC-FIX-015 |