Aller au contenu

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 :

  1. Authentification et autorisation :
  2. Les endpoints sont-ils protégés par JwtAuthGuard + AuthorizationGuard ?
  3. L'ownership est-il vérifié côté serveur (pas de trust client) ?
  4. Le actorId vient-il de user.sub (JWT claim), pas du body/query ?

  5. Injection et validation des entrées :

  6. Les requêtes SQL sont-elles paramétrées ($1, $2...) ?
  7. Le document_id est-il validé via ParseUUIDPipe ?
  8. Y a-t-il du SQL dynamique dangereux ?

  9. Concurrence et race conditions :

  10. SELECT FOR UPDATE empêche-t-il les race conditions TOCTOU ?
  11. La double vérification destruction (inclusion + exécution) est-elle effective ?

  12. Cryptographie et intégrité :

  13. Le hash SHA3-256 est-il correctement utilisé ?
  14. La canonicalisation RFC 8785 est-elle appliquée avant le hash ?
  15. Y a-t-il des comparaisons de hash non timing-safe ?

  16. Atomicité et traçabilité :

  17. L'attestation est-elle dans la même transaction que la MAJ ?
  18. Un crash entre les deux est-il possible ?
  19. L'audit post-commit est-il at-least-once ?

  20. Fuite d'information :

  21. Les messages d'erreur révèlent-ils l'état interne ?
  22. Les error_codes sont-ils acceptables (pas de PII) ?
  23. Le log inclut-il des données sensibles ?

  24. Configuration et fail-safe :

  25. Le geo_copy_count DEFAULT 0 est-il fail-safe ?
  26. La validation Joi au démarrage est-elle fail-fast ?
  27. La troncature parseInt('3.5') est-elle un risque de sécurité ?

  28. Cross-module :

  29. La garde destruction couvre-t-elle les 3 routes (INV-279-10) ?
  30. 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]