PD-279 — Revue Sécurité (7c)¶
Producteur : ChatGPT Date : 2026-03-01 Contexte : PD-279 — Restitution ISO 14641 d'un document archivé (statut RESTITUTED)
Prompt de revue¶
Tu es un auditeur sécurité senior spécialisé en applications d'archivage électronique à valeur probatoire (ISO 14641, NF Z42-013). Tu effectues une revue de sécurité pour la story PD-279.
Contexte¶
PD-279 implémente le statut RESTITUTED dans un coffre-fort numérique probatoire (ProbatioVault). Le système utilise : - NestJS + TypeORM + PostgreSQL - HSM CloudHSM pour les signatures cryptographiques - Keycloak OIDC pour l'authentification - Journal d'intégrité append-only (vault_integrity schema) - SHA3-256 pour les digests - RFC 8785 (JSON canonicalization) pour les signatures
Modèle de menaces (threat model)¶
| Menace | Scénario d'attaque | Mitigation attendue |
|---|---|---|
| Élévation de privilèges | Non-owner restitue un document | Garde ownership + SELECT FOR UPDATE |
| Contournement destruction | Document RESTITUTED détruit malgré interdiction | Triple vérification cross-module (3 routes) |
| Race condition | Appels concurrents restitute/return | SELECT ... FOR UPDATE sérialise |
| Injection SQL | document_id malveillant | ParseUUIDPipe + requêtes paramétrées |
| Replay attack | Rejeu du même POST restitute | Idempotence : 200 sans attestation |
| Déni de service SLA | Configuration SLA altérée | Joi validation stricte au démarrage |
| Audit trail incomplet | Transition sans attestation | Atomicité ACID (même transaction) |
| Fuite information | Error codes révèlent l'état interne | Codes génériques, pas de PII |
| Bypass legal_lock | Restitution d'un document sous verrou juridique | Garde legal_lock séquentielle |
| Bypass geo_copy_count | Restitution sans copies géo suffisantes | Garde geo_copy_count >= 2 |
Code source à auditer¶
1. RestitutionService — Gardes et atomicité¶
// Ordre des gardes §5.3 :
// 1. Auth (JwtAuthGuard)
// 2. document_id format (ParseUUIDPipe)
// 3. Existence → 404
// 4. Ownership → 403
// 5. État SEALED → 409 INVALID_SOURCE_STATE
// 6. Idempotence : déjà RESTITUTED → 200
// 7. legal_lock=false → 409 DOCUMENT_UNDER_LEGAL_LOCK
// 8. geo_copy_count >= 2 → 409 INSUFFICIENT_GEO_COPIES
// SELECT FOR UPDATE pour sérialiser les accès concurrents
const rows = await queryRunner.query(
`SELECT d.id, d.status, d.user_id, d.legal_lock, d.geo_copy_count, ...
FROM vault_secure.documents d WHERE d.id = $1 FOR UPDATE`,
[documentId],
);
// Ownership check
if (userId !== actorId) { throw ForbiddenException; }
// Transaction ACID : UPDATE + INSERT lifecycle_log
await queryRunner.query(`UPDATE ... SET status = $1 ... WHERE id = $4 AND status = $5`, [...]);
await this.appendJournalEntryInTransaction(queryRunner, ...);
await queryRunner.commitTransaction();
// Post-commit audit (non-bloquant)
await this.auditLogService.logAsync({ ... });
2. RestitutionController — Auth et validation¶
@UseGuards(JwtAuthGuard, AuthorizationGuard)
@Roles('user')
export class RestitutionController {
@Post(':id/restitute')
@HttpCode(HttpStatus.OK)
async restitute(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: { sub: string; security_level?: string },
): Promise<DocumentSecure> {
const securityLevel = user.security_level ?? 'standard';
return this.restitutionService.restitute(id, user.sub, securityLevel);
}
}
3. Journal d'intégrité — Attestation atomique¶
private async appendJournalEntryInTransaction(
queryRunner: QueryRunner,
archiveId: string,
eventType: JournalEventType,
payload: Record<string, unknown>,
): Promise<void> {
const canonicalPayload: string = canonicalize(payload) ?? '{}';
const payloadDigest = createHash('sha3-256').update(canonicalPayload).digest('hex');
await queryRunner.query(
`INSERT INTO vault_integrity.integrity_journal_entries
(archive_id, event_type, payload, payload_digest, timestamp)
VALUES ($1, $2, $3::jsonb, $4, NOW())`,
[archiveId, eventType, JSON.stringify(payload), payloadDigest],
);
}
4. SLA Scheduler — Idempotence¶
private async emitEventIfNotExists(
documentId: string, eventType: JournalEventType,
idempotencyKey: string, payload: Record<string, unknown>,
): Promise<boolean> {
const keyedPayload = { ...payload, _idempotency_key: idempotencyKey };
const canonicalPayload: string = canonicalize(keyedPayload) ?? '{}';
const digest = createHash('sha3-256').update(canonicalPayload).digest('hex');
const existing: unknown[] = await this.dataSource.query(
`SELECT 1 FROM vault_integrity.integrity_journal_entries
WHERE archive_id = $1 AND payload_digest = $2 LIMIT 1`,
[documentId, digest],
);
if (Array.isArray(existing) && existing.length > 0) return false;
await this.integrityJournalService.appendEntry(documentId, eventType, keyedPayload);
return true;
}
5. EligibilityService — Guard destruction cross-module¶
// PD-279 INV-279-10: Explicit rejection of RESTITUTED documents
if (status === (DocumentStatus.RESTITUTED as string)) {
return { eligible: false, reason: ExclusionReason.STATUS_RESTITUTED };
}
6. Migration DDL¶
-- Down migration precondition
SELECT COUNT(*) AS cnt FROM vault_secure.documents WHERE status = 'RESTITUTED';
-- Blocks if count > 0
Grille d'analyse sécurité¶
Pour chaque composant, analyser :
- Authentification et autorisation :
- Les endpoints sont-ils protégés par JwtAuthGuard + AuthorizationGuard ?
- L'ownership est-il vérifié côté serveur (pas de trust client) ?
-
Le
actorIdvient-il deuser.sub(JWT claim), pas du body/query ? -
Injection et validation des entrées :
- Les requêtes SQL sont-elles paramétrées ($1, $2...) ?
- Le document_id est-il validé via ParseUUIDPipe ?
-
Y a-t-il du SQL dynamique dangereux ?
-
Concurrence et race conditions :
- SELECT FOR UPDATE empêche-t-il les race conditions TOCTOU ?
-
La double vérification destruction (inclusion + exécution) est-elle effective ?
-
Cryptographie et intégrité :
- Le hash SHA3-256 est-il correctement utilisé ?
- La canonicalisation RFC 8785 est-elle appliquée avant le hash ?
-
Y a-t-il des comparaisons de hash non timing-safe ?
-
Atomicité et traçabilité :
- L'attestation est-elle dans la même transaction que la MAJ ?
- Un crash entre les deux est-il possible ?
-
L'audit post-commit est-il at-least-once ?
-
Fuite d'information :
- Les messages d'erreur révèlent-ils l'état interne ?
- Les error_codes sont-ils acceptables (pas de PII) ?
-
Le log inclut-il des données sensibles ?
-
Configuration et fail-safe :
- Le
geo_copy_count DEFAULT 0est-il fail-safe ? - La validation Joi au démarrage est-elle fail-fast ?
-
La troncature
parseInt('3.5')est-elle un risque de sécurité ? -
Cross-module :
- La garde destruction couvre-t-elle les 3 routes (INV-279-10) ?
- Un nouveau endpoint destruction sans garde est-il un risque ?
Format de sortie attendu¶
## Verdict : OK / OK AVEC RESERVES / NON CONFORME
### Analyse par catégorie OWASP / ISO 14641
| Catégorie | Statut | Détail |
|-----------|--------|--------|
| Authentification | OK/KO | ... |
| Autorisation | OK/KO | ... |
| Injection | OK/KO | ... |
| Concurrence | OK/KO | ... |
| Cryptographie | OK/KO | ... |
| Atomicité probatoire | OK/KO | ... |
| Fuite information | OK/KO | ... |
| Configuration | OK/KO | ... |
| Cross-module | OK/KO | ... |
### Vulnérabilités identifiées
| ID | Catégorie | Gravité | Description | Recommandation |
|----|-----------|---------|-------------|----------------|
| ... | ... | CRITIQUE/HAUTE/MOYENNE/BASSE | ... | ... |
### Points positifs
- [liste]
### Recommandations
- [liste]