Picarones / docs /security /threat-model.md
Claude
chore(versioning): reposition project as 0.9.0 (pre-1.0)
416bee1 unverified

Threat model — Picarones

Audience : DSI institutionnelle (BnF, LoC, BL), auditeur sécurité, mainteneur. Ce document complète /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).
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 pour le canal de divulgation responsable. La version anglaise est dans /SECURITY.en.md.

Révisions

Version Date Changements
1.0 2026-05 Création initiale (S60), méthodologie STRIDE