# Threat model — Picarones > **Audience** : DSI institutionnelle (BnF, LoC, BL), auditeur > sécurité, mainteneur. Ce document complète > [`/SECURITY.md`](../../SECURITY.md) en formalisant le modèle de > menace. Méthodologie : **STRIDE** (Microsoft) + adaptation > patrimoine numérique. > > **Périmètre** : déploiement institutionnel — Picarones tourne sur > une infrastructure interne (NAS, cluster Kubernetes), un workspace > partagé entre chercheurs, des clés API cloud côté serveur. > > **Hors périmètre** : déploiement public HuggingFace Space (mode > ouvert anonymisé, sans secrets), CLI mono-utilisateur en local > (modèle de menace = celui de la machine de l'utilisateur). > > **Statut** : itération 1, 2026-05 (couvre `0.9.0`). À réviser à > chaque release majeure ou incident sécurité. ## Acteurs | Acteur | Confiance | Capacités | |--------|-----------|-----------| | **Utilisateur authentifié** (chercheur, archiviste BnF) | Modéré | Upload corpus, lance benchmark, lit rapport, télécharge artefacts | | **Utilisateur invité** (lecteur d'un rapport publié) | Bas | Lit un rapport HTML produit | | **Opérateur** (DSI institutionnelle) | Élevé | Déploie, configure, accède aux logs, gère les clés API | | **Mainteneur** (équipe Picarones) | Élevé sur le code | Push code, release, accès limité aux instances de production | | **Attaquant externe** | Aucune | Internet public ou utilisateur malveillant | ## Actifs à protéger | Actif | Sensibilité | Pourquoi | |-------|-------------|----------| | **Corpus uploadés** | RGPD (peut contenir PII : registres d'état civil) | Article 4 RGPD — données personnelles si nominatives | | **Vérités terrain (GT)** | Propriété intellectuelle de l'institution | Investissement humain coûteux ; secret de fait | | **Clés API cloud** (`OPENAI_API_KEY`, etc.) | Secret crédential | Compromission = facturation arbitraire + exfiltration de données | | **Résultats de benchmark** | Faible (résultats agrégés) | Sauf si attribués nominativement à un transcripteur | | **Logs applicatifs** | Modéré (PII collatéral, métadonnées corpus) | Audit trail = preuve juridique mais aussi cible | | **Code source** | Public (OSS) | Intégrité supply-chain (signed releases, SBOM, SLSA) | | **Base SQLite des jobs** | Modéré (historique des runs, paramètres) | Permet de reconstituer l'activité d'un utilisateur | ## Surfaces d'attaque ``` ┌──────────────────────────────────────────────────────────┐ │ Internet / Intranet │ └─────────────────────┬────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────┐ │ FastAPI (interfaces/web) │ ← S1 (HTTP), S2 (auth) │ - SecurityHeadersMiddleware │ │ - BodySizeLimitMiddleware │ │ - RateLimitMiddleware │ │ - AuthenticationMiddleware (opt-in) │ └────────────────────┬──────────────────┘ │ ▼ ┌───────────────────────────────────────┐ │ RunOrchestrator + JobRunner │ ← S3 (job exec) │ - WorkspaceManager (sandbox) │ │ - ZIP extraction (zip-slip safe) │ └────────────────────┬──────────────────┘ │ ┌───────────────┼─────────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌───────────┐ ┌─────────────┐ │ Adapters │ │ Adapters │ │ Storage │ ← S4 (cloud) │ OCR cloud│ │ LLM cloud │ │ filesystem │ ← S5 (FS) │ (HTTPS) │ │ (HTTPS) │ │ + SQLite │ ← S6 (DB) └──────────┘ └───────────┘ └─────────────┘ ``` ## Menaces — analyse STRIDE ### S — Spoofing (usurpation d'identité) | ID | Menace | Mitigation | |----|--------|------------| | S1 | Un attaquant se fait passer pour un utilisateur authentifié | `AuthenticationMiddleware` opt-in avec `AuthenticationBackend` Protocol — l'institution branche son SSO/LDAP/JWT. Les endpoints `/health` et `/version` restent publics pour les sondes. | | S2 | Un client forge `X-Forwarded-For` pour spoofer son IP dans le rate limit | `RateLimitMiddleware.trust_proxy_count: int` (défaut 0 = XFF ignoré). Lecture du Nème IP en partant de la fin de la chaîne XFF. Test `tests/interfaces/web/test_rate_limit_xff.py` (7 cas). | | S3 | Un attaquant publie un faux package `picarones` sur PyPI | Le projet n'est pas encore sur PyPI public. À la publication : signer les wheels avec Sigstore et publier le SLSA provenance level 3 (cf. backlog). | ### T — Tampering (altération) | ID | Menace | Mitigation | |----|--------|------------| | T1 | Un utilisateur uploade un ZIP avec des chemins zip-slip pour écrire hors workspace | `WorkspaceManager` sandboxe par session, extraction ZIP filtre les chemins absolus et `..`. | | T2 | Un caller construit `DocumentRef(id="../../etc/passwd")` programmatiquement | `_DOC_ID_RE` regex `^[A-Za-z0-9_.\-/]+$` + validateur Pydantic explicite qui rejette tout segment `..` (S59 #M3). | | T3 | Un attaquant altère le schéma SQLite `jobs.db` entre deux démarrages | `JobStore.SCHEMA_VERSION` + dispatcher `_MIGRATIONS` qui rejette dur les schémas downgrade. Pas de mitigation contre une altération en place — c'est au filesystem. | | T4 | Un cache d'artefact corrompu ferait diverger un run | `ArtifactKey.hash_hex()` multi-paramètres (inputs hash + step + code_version + params + projection_spec) — un cache pollué est rejeté à la lecture parce que la clé ne match plus. | | T5 | Une fonte / modèle local est remplacé par un fichier malveillant | Picarones ne charge aucun modèle automatiquement. Les modèles Tesseract et Pero sont pointés explicitement par l'utilisateur ; à charge à lui de vérifier les hashes. | ### R — Repudiation (non-répudiation) | ID | Menace | Mitigation | |----|--------|------------| | R1 | Un utilisateur lance un job coûteux puis nie l'avoir fait | `[audit]` log INFO sur `POST /api/jobs` et `DELETE /api/jobs/{id}` avec IP source (S59 #M2). Logs structurés à conserver côté ops selon la politique RGPD. | | R2 | Un attaquant modifie un rapport persisté pour falsifier les chiffres | Le `RunManifest` est byte-déterministe (`model_dump_json` Pydantic ordered). Le hash SHA-256 du manifest peut être cité dans une publication pour ancrer la version. Signature cryptographique : non implémentée, à arbitrer (cf. backlog). | | R3 | Un mainteneur publie une release sans laisser de trace | GitHub Actions `release.yml` enregistre l'identité GitHub du déclencheur ; SLSA provenance (à venir) attestera la chaîne build → wheel. | ### I — Information disclosure | ID | Menace | Mitigation | |----|--------|------------| | I1 | Une clé API cloud (`OPENAI_API_KEY`, etc.) fuit dans un log applicatif | Les adapters ne logent jamais la clé — vérifié par revue de code. Les exceptions cloud sont catchées et le message reformulé sans inclure de header. À durcir : un test `bandit` dans la CI sur les patterns `api_key` en variable de log. | | I2 | Un rapport HTML embarque un CSP permissif et leak via XSS | `CSP: default-src 'self'`, pas de `unsafe-inline`, vérifié par `tests/interfaces/web/test_sprint_a14_s49_security.py`. Le moteur narratif rend les chiffres via templates YAML (pas de injection HTML). | | I3 | Le workspace partagé fait fuiter le corpus d'un chercheur à un autre | `WorkspaceManager` sandboxe par `session_id` ; aucun caller ne peut sortir de son workspace via `resolve_output_path`. | | I4 | Un endpoint `GET /api/jobs/{job_id}` divulgue les paramètres d'un autre utilisateur | Pas d'isolation multi-tenants à ce jour — défaut documenté. Le déploiement institutionnel doit ajouter une couche d'autorisation par utilisateur (cf. `AuthenticationMiddleware`). | | I5 | Un attaquant lit `dependencies_lock` du `RunManifest` pour cibler une CVE | Acceptable — `dependencies_lock` est public par design (reproductibilité). La défense est de patcher rapidement les CVE via `pip-audit` en CI. | ### D — Denial of Service | ID | Menace | Mitigation | |----|--------|------------| | D1 | Upload ZIP géant qui sature le disque | `BodySizeLimitMiddleware` (défaut 100 MiB). **Limite connue** : ne couvre pas `Transfer-Encoding: chunked` — recommandation = nginx `client_max_body_size` en amont (cf. [`operations/runbook.md`](../operations/runbook.md)). | | D2 | Flood de requêtes saturant le rate limit en mémoire | `RateLimitMiddleware` avec eviction LRU `max_clients=10000` (S58). Pas atomique sous très haute concurrence — best-effort assumé. | | D3 | Job qui hang sur appel cloud (timeout réseau) | `pytest-timeout 5 min` par test ; `urllib.request.urlopen(timeout=)` configurable par adapter ; `call_with_retry` partagé (3 retries 2/4/8s) qui FAIL fast si non-retryable. | | D4 | DAG cyclique ou infini dans une `PipelineSpec` | Validation statique avec détection de cycle dans `pipeline/validation.py` ; rejet `PipelineSpecError` au load. | | D5 | XML billion-laughs / XXE sur upload ALTO/PAGE | `defusedxml` exclusif dans `formats/alto/parser.py` et `formats/pagexml/parser.py`. | ### E — Elevation of privilege | ID | Menace | Mitigation | |----|--------|------------| | E1 | Un module contribué tiers s'exécute avec des privilèges qu'il ne devrait pas | `BaseModule` interface stricte ; `module_policy.audit_module` valide qu'un module externe ne dérive que de `BaseModule` et déclare ses `input_types`/`output_types` proprement. Pas de sandboxing process — un module malicieux peut faire `os.system`. | | E2 | Un utilisateur web arrive à exécuter du code arbitraire via l'API | `RunSpec` est validé par Pydantic ; `adapter_class` est un dotted-path résolu via `importlib.import_module` mais filtré contre une liste explicite via `RegistryService.bootstrap_defaults()`. Une release institutionnelle doit verrouiller cette liste. | ## Risques résiduels acceptés | ID | Risque | Pourquoi accepté | |----|--------|------------------| | RR1 | Le rate limit n'est pas atomique sous très haute concurrence | Best-effort suffit pour usage institutionnel ; un Redis-backed rate limiter est l'évolution si besoin | | RR2 | Un module Python contribué peut faire des `os.system` arbitraires | Le modèle de confiance est *« le mainteneur a revu le code »* — pas de sandbox process. Pour un usage institutionnel multi-tenant, déployer dans un conteneur isolé par tenant. | | RR3 | Les clés API cloud sont en variables d'environnement, pas en HSM | Standard de l'industrie ; un Vault-backed secret store est l'évolution si la DSI l'exige. | | RR4 | Pas d'isolation multi-tenants par user dans le workspace web | Documentée explicitement ; déploiement multi-tenants doit ajouter sa propre couche d'autorisation. | ## Procédure de signalement Voir [`/SECURITY.md`](../../SECURITY.md) pour le canal de divulgation responsable. La version anglaise est dans [`/SECURITY.en.md`](../../SECURITY.en.md). ## Révisions | Version | Date | Changements | |---------|------|-------------| | 1.0 | 2026-05 | Création initiale (S60), méthodologie STRIDE |