File size: 12,128 Bytes
d0a3fab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416bee1
 
d0a3fab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# 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 |