PD-275 — Revue Sécurité
Résumé
| Critère | Statut |
| Forbidden patterns | ⚠️ |
| Injection SQL | ✅ |
| Auth/Authz | ⚠️ |
| Fuite données | ✅ |
| Validation | ⚠️ |
Verdict : ⚠️ RÉSERVES
Contrainte globale : l'implémentation est globalement robuste côté SQLi/concurrence/traçabilité, mais 3 écarts de sécurité (dont 1 écart documenté de stub) empêchent un verdict CONFORME.
Audit des forbidden patterns
| Pattern interdit | Recherché | Trouvé |
| Subquery WHERE d'index partiel | Migrations PD-275 | ✅ Absent |
| Type-change non réversible | Migrations PD-275 | ✅ Absent |
| DROP TABLE/COLUMN en up | Migrations PD-275 | ✅ Absent |
| Transition REVOKED -> * / état REACTIVATED | signer-status.enum.ts | ✅ Absent |
| Lecture sans verrou pessimiste (revoke/assertActive) | signer-registry.service.ts | ✅ Absent |
| revokedBy depuis paramètre externe | signer.controller.ts, service | ✅ Absent (body rejeté, valeur JWT sub) |
| Finalisation sans garde de seuil | anchor-batch.service.ts | ✅ Absent |
| FINALITY_DEPTH en dur (non configurable) | finality-guard.service.ts | ✅ Configurable via ConfigService |
| Route sans OidcJwtAuthGuard / RolesGuard | signer.controller.ts | ⚠️ Pas de garde locale explicite (dépendance APP_GUARD + check service) |
Tentatives de bypass
| Attaque | Résultat | Commentaire |
{ "revokedBy":"hack@evil.com" } | Rejet (400) | Anti-spoofing explicite via req.body + code ERR-REVOKEDBY-SPOOFING |
address="'; DROP TABLE--" | Non exploitable | Requêtes TypeORM paramétrées (findOne/update/save) |
| Requête sans JWT | Refus (403) | Fail-closed via absence req.user.sub |
| JWT sans rôle ADMIN/SIGNER_ADMIN | Refus | Check OR côté service + audit deny |
| Double revoke concurrent | 1 succès + 1 conflit attendu | SELECT ... FOR UPDATE + test statut REVOKED |
| Finalize sans confirmations suffisantes | Refus | assertFinalityReached() + audit FINALITY_CHECK_FAILED |
| Submit signer révoqué (si signerAddress présent) | Refus | assertSignerActive() sous transaction |
| Submit signer révoqué (signerAddress absent) | ⚠️ Bypass possible | Le check est conditionnel (if (signerAddress)) |
Vulnérabilités identifiées
| ID | Description | Gravité | Fichier |
| S-01 | Bypass possible du contrôle signer actif si signerAddress est indéfini : process() saute assertSignerActive, puis submitBatch() saute aussi le contrôle (conditionnel). Vecteur : config sans anchor.signerAddress + intégration wallet non finalisée. Impact : ancrage possible sans preuve runtime de statut ACTIVE. | MAJEUR | src/modules/anchor/processors/blockchain-anchor.processor.ts, src/modules/anchor/services/anchor-batch.service.ts |
| S-02 | finalityDepth non borné/validé au démarrage. Vecteur : config anchor.finalityDepth <= 0 (ou incohérente) → garde de finalité neutralisable logiquement. Impact : finalisation prématurée (violation INV-275-02) en cas de dérive config. | MAJEUR | src/modules/anchor/services/finality-guard.service.ts |
| S-03 | Validation d'entrée incomplète sur address (format EIP-55 non enforced). Vecteur : entrée arbitraire/oversize dans paramètre path → bruit d'audit/log, erreurs métier évitables, surface DoS applicative accrue. Impact limité mais réel sur robustesse sécurité. | MINEUR | src/modules/anchor/controllers/signer.controller.ts |
Recommandations
- [S-01] Forcer un mode fail-closed strict sur signer : si
resolveSignerAddress() ne résout rien, lever une erreur bloquante (pas de fallback permissif). - [S-02] Valider
anchor.finalityDepth au bootstrap (entier, >=1, borne max raisonnable) et échouer au démarrage si invalide. - [S-03] Ajouter validation forte sur
:address (regex Ethereum/EIP-55 + longueur stricte) via pipe dédié. - [Defense-in-depth] Ajouter
@UseGuards explicite côté contrôleur + mécanisme rôle OR dédié pour supprimer la dépendance implicite aux guards globaux. - [Points forts] Conserver les acquis : verrou pessimiste, anti-spoofing
revokedBy, audit d'échecs, transitions terminales REVOKED.