Picarones / SECURITY.md
Claude
feat(web): Sprint A4 — sécurité web (B-11 CSRF, M-3 /health)
c9d381c unverified
|
Raw
History Blame
7.44 kB
# SECURITY — Picarones
Picarones est conçu pour être déployé dans trois contextes très différents :
1. **Poste développeur** (Codespaces, laptop) — accès local uniquement, le
garde-fou est ouvert pour fluidifier l'itération.
2. **Serveur d'institution** (intranet patrimonial, cluster scientifique) —
accès authentifié interne, mais quelques utilisateurs peuvent lancer des
benchmarks coûteux ; le serveur doit borner la consommation.
3. **Espace public** (HuggingFace Space, démo en ligne) — n'importe quel
visiteur peut atteindre l'API ; les clefs serveur (OpenAI, Anthropic,
Mistral, Azure…) ne doivent **pas** être exposées au DoS-financier.
Ce document décrit les contrôles disponibles depuis le **Sprint 24** et la
configuration recommandée pour chaque cas.
---
## Variables d'environnement de sécurité
| Variable | Défaut | Effet |
|----------|--------|-------|
| `PICARONES_PUBLIC_MODE` | non défini | Si `1`/`true` : refuse OCR cloud + LLM mutualisés et active rate limit. |
| `PICARONES_BROWSE_ROOTS` | (auto) | Liste de chemins (séparateur `:` Unix / `;` Windows) autorisés pour `/api/corpus/browse`. Surcharge le défaut. |
| `PICARONES_MAX_UPLOAD_MB` | `100` | Taille max d'une image uploadée. |
| `PICARONES_MAX_CONCURRENT_JOBS` | `2` | Plafond global de benchmarks simultanés (sémaphore en mémoire). |
| `PICARONES_RATE_LIMIT_PER_HOUR` | `5` (en mode public) | Jobs max par IP et par heure. `0` = désactivé. |
| `PICARONES_CSP` | (politique durcie) | Surcharge la `Content-Security-Policy` envoyée par le middleware. |
| `PICARONES_CSRF_REQUIRED` | non défini | Si `1`/`true`/`yes` : active la protection CSRF (double-submit cookie + signature HMAC) sur tout POST/PUT/PATCH/DELETE. Voir § « CSRF — déploiement institutionnel » ci-dessous. |
| `PICARONES_CSRF_SECRET` | (auto) | Secret HMAC pour signer les tokens CSRF. Si non défini, généré au démarrage avec un warning ; les tokens sont alors invalidés à chaque redémarrage. **À définir en production**. |
---
## CSRF — déploiement institutionnel (Sprint A4)
L'application embarque un middleware CSRF **désactivé par défaut**
(rétrocompat HuggingFace Space où il n'y a pas de session
authentifiée à protéger). Pour un déploiement BnF / Bibliothèque
nationale derrière SSO :
```bash
export PICARONES_CSRF_REQUIRED=1
export PICARONES_CSRF_SECRET="$(openssl rand -hex 32)" # 64 chars hex
```
**Comment ça marche** : pattern « double-submit cookie ». Le serveur
pose un cookie `picarones_csrf` (httponly=False, samesite=strict) qui
contient un token `<nonce>.<HMAC-SHA256(secret, nonce)>`. Sur tout
POST/PUT/PATCH/DELETE non exempt, le client doit renvoyer le même
token dans l'en-tête `X-CSRF-Token`. Le serveur compare en temps
constant et vérifie la signature. Une page tierce ne peut pas lire
le cookie (samesite=strict + JS d'origine différente) ni produire
une signature valide (HMAC), donc ne peut pas forger une requête.
**Endpoints exemptés** : `/health`, `/api/csrf/token` (le endpoint
qui *donne* le token).
**Bootstrap d'un client tiers** (curl, scripts CI) :
```bash
# 1. Récupérer un token et persister le cookie dans un jar
curl -c cookies.txt http://picarones.example/api/csrf/token | jq -r .token
# 2. Réutiliser le token dans le header
TOKEN=$(jq -r .token < <(curl -sb cookies.txt http://.../api/csrf/token))
curl -b cookies.txt -H "X-CSRF-Token: $TOKEN" -X POST .../api/lang/fr
```
**Frontend** : le JS embarqué (`web-app.js`) wrappe `fetch()` pour
injecter automatiquement le header sur toute requête mutante
same-origin. Aucun changement requis dans le code applicatif.
---
## Contrôles par contexte
### 🧑‍💻 Développement (défaut, `PICARONES_PUBLIC_MODE` non défini)
```bash
picarones serve --port 8000
```
- Tous les moteurs OCR sont disponibles.
- `/api/corpus/browse` voit `cwd`, `./uploads/`, `/workspaces`, `tempdir`.
- Pas de rate limit.
- CSP appliquée mais permissive (`unsafe-inline` toléré tant que les
templates web ne sont pas Jinja2 — voir Sprint 25).
### 🏛 Serveur d'institution
```bash
export PICARONES_BROWSE_ROOTS="/srv/corpus:/srv/uploads"
export PICARONES_MAX_CONCURRENT_JOBS=4
export PICARONES_MAX_UPLOAD_MB=500
picarones serve --host 0.0.0.0 --port 8000
```
À combiner avec une terminaison TLS et une authentification au niveau
reverse-proxy (nginx + auth basic, ou Keycloak/SAML). Picarones n'embarque
pas son propre système d'authentification — c'est un choix conscient pour
ne pas réinventer un sous-système qui sera mieux servi par l'infra existante.
### 🌐 HuggingFace Space / démo publique
```dockerfile
ENV PICARONES_PUBLIC_MODE=1
ENV PICARONES_RATE_LIMIT_PER_HOUR=5
ENV PICARONES_MAX_CONCURRENT_JOBS=2
ENV PICARONES_MAX_UPLOAD_MB=50
# Optionnel : surcharger les browse roots
# ENV PICARONES_BROWSE_ROOTS=/data/corpus
```
Effets en mode public :
- ❌ Moteurs OCR cloud (`mistral_ocr`, `google_vision`, `azure_doc_intel`)
refusés en `403`.
- ❌ Pipelines OCR+LLM (`openai`, `anthropic`, `mistral`, `ollama`)
refusés en `403`.
-`/api/corpus/browse` se limite à `./uploads/`.
-`/api/benchmark/start` et `/api/benchmark/run` rate-limités en `429`.
- 🔒 `Content-Security-Policy` + `X-Frame-Options: DENY` +
`X-Content-Type-Options: nosniff` + `Referrer-Policy: strict-origin-when-cross-origin`
sur toutes les réponses.
---
## Contrôles d'upload
### Images
- **Validation Pillow** systématique : `Image.open(...).verify()` dans
un `try/except` qui capture les `UnidentifiedImageError`,
`DecompressionBombError`, et l'exception générique.
- **Limite de taille** par fichier (`PICARONES_MAX_UPLOAD_MB`).
- **Basename forcé** : un nom de fichier multipart contenant `..` ou `/`
est tronqué à son nom de base avant écriture.
### Archives ZIP
- **Bombe ZIP** : taille décompressée bornée à 500 Mo, nombre de fichiers
borné à 2000.
- **Path traversal** : seuls les noms de base sont conservés (les répertoires
internes du ZIP sont aplatis).
- **Filtres macOS** : les fichiers `._*` (AppleDouble) sont ignorés.
- **Symlinks** : Python's `zipfile` n'extrait pas les symlinks par défaut ;
un check explicite (`ZipInfo.external_attr & 0xA000`) est sur la roadmap
comme défense en profondeur.
---
## Modèle de menace
| Menace | Mitigation |
|--------|-----------|
| Visiteur consomme la clef API mainteneur | `PICARONES_PUBLIC_MODE=1` → 403 sur LLM/OCR cloud. |
| DoS via 50 benchmarks concurrents | `PICARONES_MAX_CONCURRENT_JOBS` (sémaphore) + rate limit par IP. |
| Bombe Pillow (`CVE-2023-50447` & cie) | `Image.verify()` levant `DecompressionBombError`. |
| Path traversal sur browse / image / delete | Validation explicite + résolution + check `is_relative_to`. |
| Exfiltration via browse `/etc` ou `/root` | `PICARONES_BROWSE_ROOTS` restreint, défaut public limité à uploads. |
| XSS via paramètres URL | CSP `default-src 'self'`, `frame-ancestors 'none'`. |
| Clickjacking | `X-Frame-Options: DENY`. |
---
## Reporting de vulnérabilités
Les vulnérabilités potentielles peuvent être ouvertes via une *Security
Advisory* GitHub (privée par défaut) sur
[github.com/maribakulj/Picarones](https://github.com/maribakulj/Picarones).
Merci de **ne pas** divulguer publiquement avant qu'un correctif ne soit
disponible. Les contributeurs prendront en charge la triage en moins de
14 jours.