Audit de sécurité itératif : 45 probes, 0 critical, 6 tests de régression gardés

Tout au long de cette série, j'ai partagé des patterns découverts pendant un audit de sécurité sur un service d'authentification Go : PKCS#12, timing oracle, lockout, CSRF, mTLS, CRL, CQRS. Parlons maintenant de la méthodologie elle-même : comment on a conduit l'audit, et comment on a transformé les résultats en tests permanents.

L'audit en passes successives

L'audit n'est pas un scan unique. C'est un processus itératif en passes, chacune avec un objectif différent et un rendement décroissant :

  1. Analyse statique — lire le code, identifier les patterns de sécurité (ou leur absence), noter les questions
  2. Reconnaissance — comprendre les flux, les dépendances, les frontières de confiance, les configurations TLS/session/auth
  3. Analyse critique — challenger chaque finding de la passe 1, vérifier si les mitigations existent
  4. Stabilisation — corriger les vrais findings, documenter les faux positifs, préparer le runtime
  5. Probes runtime actives — lancer des requêtes réelles contre une instance de test

Chaque passe rapporte du nouveau. La passe 1 trouve les patterns manquants ("pas de CSRF token"). La passe 2 découvre que le CSRF token est dans un middleware qu'on n'avait pas vu. La passe 3 vérifie que le middleware est dans le bon ordre. La passe 4 confirme que l'ordre est correct. La passe 5 vérifie que ça tient sous une vraie requête.

Vérifier les findings avant de paniquer

J'ai appris ça à la dure : un outil d'analyse (ou un audit sub-agent) te sort un finding "CRITICAL: timing leak on login endpoint". Tu paniques. Tu passes 4 heures à instrumenter le code. Résultat : le "timing leak" est un delta de 5ms dû à la latence réseau, pas au code.

La discipline : chaque finding doit passer par un cycle de vérification avant d'être escaladé.

  1. Reproduire — le finding est-il reproductible de manière fiable ?
  2. Isoler — le problème est-il dans le code ou dans l'environnement de test ?
  3. Mesurer — pour les findings de timing, 50 mesures minimum, pas une seule
  4. Contextualiser — le finding est-il exploitable dans le contexte réel (avec mTLS, rate limiting, etc.) ?

Sur cet audit, la passe d'analyse statique a produit 23 findings potentiels. Après vérification : 8 vrais findings, 15 faux positifs. Ratio 35% de vrais positifs. C'est normal — un audit qui ne produit pas de faux positifs n'a probablement pas cherché assez large.

Comment on produit ses propres faux positifs

Le piège le plus vicieux : se tromper soi-même. Un exemple concret de cet audit :

J'ai mesuré le timing du endpoint de login avec un lockout actif. Le 6ème échec prenait 55ms, le 7ème prenait 52ms. Delta : 3ms. Mon premier réflexe : "lockout bypass — le timing varie, donc on peut distinguer les tentatives lockées des tentatives non-lockées".

Sauf que non. Le delta de 3ms était du bruit statistique. Sur 100 mesures, la moyenne était identique (54ms ± 4ms) que le compte soit locké ou non. Le dummy hash fonctionnait parfaitement. J'avais failli créer un finding à partir de rien.

La leçon : le cerveau humain est un excellent pattern matcher, y compris sur du bruit. Les mesures de timing doivent toujours être statistiques (N > 30, comparaison de moyennes) et jamais basées sur une ou deux observations.

De l'analyse statique aux probes runtime

Quand passer des passes statiques aux probes actives ? Quand tu as épuisé ce que le code peut te dire et que tu as besoin de voir comment le système se comporte en vrai.

Les signaux pour basculer :

  • Les passes statiques ne produisent plus de nouveaux findings depuis 2 itérations
  • Tu as des hypothèses que seul le runtime peut confirmer (timing, race conditions, behaviour sous charge)
  • Les mitigations identifiées en statique doivent être validées en conditions réelles

Sur cet audit, on est passé au runtime après 4 passes statiques. La passe runtime a confirmé 7 des 8 findings et en a ajouté 1 nouveau (un edge case sur le CRL reload que le code ne rendait pas évident).

45 probes → 6 regression tests

Pendant la phase runtime, on a lancé 45 probes contre une vraie instance. Résultat : 0 vulnérabilité Critical ou High. Tous les findings étaient déjà corrigés ou étaient des observations informatives.

La question : quelles probes garder en regression suite permanente ?

Garder : les patterns non couverts par les E2E existants

  • Timing consistency — mesurer |unknown - known| < seuil sur le login
  • Homoglyphes Unicode — tenter un login avec des caractères Unicode visuellement identiques (ex: 'а' cyrillique vs 'a' latin)
  • Multi-CSRF fields — envoyer plusieurs tokens CSRF dans la même requête pour vérifier que le serveur n'en accepte qu'un
  • Host header injection — vérifier le 421 Misdirected Request quand Host != SNI
  • Duplicate Origin header — envoyer deux headers Origin pour tester la résistance du CORS
  • Conditional GET ETag — vérifier que les réponses authentifiées ne sont pas cachées par un proxy via ETag

Supprimer : les patterns déjà couverts

  • Rate limiting (déjà testé dans les E2E du rate limiter)
  • Path traversal (couvert par les tests du router)
  • XSS (couvert par les tests de templating + CSP headers)
  • SQL injection (couvert par les tests du query builder)
  • RBAC (couvert par les E2E de permissions)

Le ratio : 45 probes → 6 regression tests gardés. 13% de rétention. Les 87% restants sont soit redondants avec les E2E existants, soit des vérifications one-shot qui n'ont de sens que pendant l'audit initial.

Le critère de sélection

Pour chaque probe, la question à se poser :

Est-ce qu'un changement de code futur, fait de bonne foi par un développeur qui ne connaît pas ce finding, pourrait réintroduire la vulnérabilité ?

Si oui → regression test. Si non (parce que le framework l'empêche, ou parce que ça nécessiterait un changement délibéré et visible) → pas de test.

Le test de timing est l'exemple parfait : un refactoring du handler de login pourrait facilement oublier le dummy hash. Le test de path traversal, en revanche, ne serait cassé que par un changement du router — un changement tellement visible qu'il serait reviewé par toute l'équipe.

Conclusion

Un audit de sécurité n'est pas un scan. C'est un processus itératif où chaque passe affine la compréhension, et où la discipline de vérification des findings évite de perdre du temps sur des fantômes.

Le vrai livrable d'un audit n'est pas le rapport — c'est les 6 tests de régression qui survivent dans la CI et qui empêchent les régressions silencieuses. Le rapport se lit une fois. Les tests tournent à chaque commit.

Dernier article de cette série : après l'audit, comment documenter tout ça pour les agents IA qui toucheront au code ? La discipline du CLAUDE.md — 296 → 142 lignes, et mon agent code mieux qu'avant. C'est le sujet du prochain article.

Commentaires (0)