Spaces:
Running
Running
File size: 7,435 Bytes
d86e268 c9d381c d86e268 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | # 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.
|