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

200 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` : 14094 (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 (`&#9632;`, `&#9670;`, `&#9635;`) 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 |