Écrire et tester du code sûr
Rendre le code sûr par construction grâce aux frameworks et à la simplicité, puis le prouver par les tests, le fuzzing et l'analyse statique.
Le code contiendra toujours des bogues : c'est une constante du métier, et aucune dose de talent ou de vigilance ne l'efface. La proposition centrale de cette partie « Implémentation » du livre est donc de déplacer le problème — non pas demander à chaque ingénieur de ne jamais se tromper, mais concevoir l'environnement de développement de sorte que les classes entières d'erreurs deviennent impossibles ou immédiatement visibles. On y parvient en deux temps complémentaires : rendre le code sûr par construction (frameworks durcis, simplicité, bons outils), puis le prouver par un arsenal de tests et d'analyses. Ce chapitre suit le fil rouge d'une étude de cas Google — la conception d'une autorité de certification (certificate authority, CA) publiquement reconnue — avant d'exposer les principes d'écriture (chapitre 12 du livre) puis de test (chapitre 13).
Étude de cas : une autorité de certification publiquement reconnue
Une autorité de certification publiquement reconnue (publicly trusted CA) est un ancre de confiance (trust anchor) pour la couche transport d'Internet : elle émet les certificats TLS, S/MIME et autres que navigateurs, systèmes d'exploitation et appareils acceptent par défaut. Pour mériter ce statut et le conserver, une CA doit passer des audits exigeants (WebTrust, ETSI, exigences de base du CA/Browser Forum) — au point qu'une CA typique y consacre au moins un trimestre par an. C'est, par nature, une infrastructure aux exigences de sécurité et de fiabilité maximales.
Votre organisation n'aura presque jamais à bâtir une telle CA — la plupart s'appuient sur des tiers pour leurs certificats publics. L'intérêt de ce récit n'est donc pas le « comment construire une CA », mais les principes transposables qu'il illustre. Google a longtemps acheté ses certificats à un tiers, jusqu'à ce que trois problèmes le poussent à internaliser : la dépendance à des tiers dont on ne maîtrisait pas le niveau de sûreté (l'effondrement de DigiNotar après compromission est resté dans les mémoires), le besoin d'automatisation (des milliers de domaines à protéger, des certificats à faire tourner souvent, des API tierces souvent inextensibles) et le coût rapporté aux millions de certificats nécessaires.
Note
Le livre prévient honnêtement que cette étude de cas présente une image « idéalisée ». Les choix décrits ont en réalité été faits sur près d'une décennie, en trois itérations distinctes. On ne peut pas toujours concevoir tout d'emblée ; ce qui compte, c'est de poser de bons principes dès le départ, de commencer petit et d'itérer continûment sur les propriétés de sécurité et de fiabilité.
Choisir un langage à sûreté mémoire
Le choix du langage de programmation pour les composants qui acceptent une entrée arbitraire non fiable (untrusted input) fut un aspect décisif de la conception. Google a écrit la CA dans un mélange de Go et de C++, en arbitrant sous-composant par sous-composant. Le motif est limpide : les requêtes de signature de certificat (Certificate Signing Requests, CSR) représentent une entrée non fiable, qui peut provenir d'un système interne relativement sûr comme d'un internaute potentiellement malveillant. Or l'historique des vulnérabilités liées à la mémoire dans le code de parsing du format DER (Distinguished Encoding Rules) est interminable — la base CVE en recense des centaines. D'où le recours à un langage à sûreté mémoire (memory-safe) comme Go pour cette frontière.
Le C++, lui, n'est pas à sûreté mémoire mais offre une interopérabilité précieuse avec l'infrastructure critique de Google. Pour le sécuriser, on l'exécute dans une zone sûre (secure zone) et on valide toute donnée avant qu'elle n'y entre. Pour le traitement des CSR, Google adopte une élégante défense en profondeur : la requête est analysée en Go puis relayée au sous-système C++ pour la même opération, et les deux résultats sont comparés ; à la moindre divergence, le traitement s'arrête.
CSR (entrée non fiable)
│
┌─────────────┴─────────────┐
▼ ▼
Parseur Go (memory-safe) Parseur C++ (zone sûre)
│ │
└─────────────┬─────────────┘
▼
comparaison des résultats
├─ identiques → on continue
└─ divergents → on arrête À cela s'ajoutent des protections à la compilation et à l'exécution offertes par la chaîne d'outils centralisée de Google : W^X (qui casse l'astuce d'exploitation consistant à mmaper avec PROT_EXEC, sans coût CPU ni mémoire), l'allocateur de tas sécurisé Scudo, et SafeStack (protection contre les débordements de tampon de pile).
Segmentation, simplicité et composants tiers
L'architecture de la CA repose sur trois couches (analyse de la requête, fonctions d'autorité d'enregistrement, signature du certificat), chacune composée de microservices aux responsabilités bien définies, et sur une double zone de confiance où l'entrée non fiable est traitée dans un environnement distinct des opérations critiques. Cette segmentation crée des frontières claires qui facilitent la revue et compliquent toute attaque : un attaquant qui compromet un composant à fonctionnalité réduite ne peut affecter que cette fonctionnalité, et doit franchir d'autres points d'audit pour aller plus loin.
Le principe directeur de chaque microservice est la simplicité, par mesure défensive. Plutôt que d'embrasser toute l'étendue des standards, Google a délibérément implémenté une CA à fonctionnalité limitée : son usage principal était l'émission de certificats web standards, et les options ésotériques des logiciels commerciaux engendraient une complexité qui les rendait difficiles à valider et plus sujets aux erreurs. La quête de simplicité est continue : on a fini par consolider des microservices devenus trop nombreux (coûts de maintenance), et centralisé des vérifications d'ACL pour les appels RPC qui, implémentées manuellement dans chaque cas, ouvraient la porte aux oublis de développeur ou de relecteur.
Le code tiers — bibliothèques open source et modules commerciaux — devait lui aussi être validé, durci et conteneurisé. Même des paquets largement utilisés en contexte de sécurité sont vulnérables : Google a mené une revue approfondie de chacun et soumis des correctifs. Le parseur de CSR, qui s'appuie sur des bibliothèques X.509 open source, tourne comme microservice dans la zone non fiable, en conteneur Borg — une couche de protection supplémentaire. Pour le code propriétaire fermé du module matériel de sécurité (hardware security module, HSM) — le coffre cryptographique vendu par un tiers —, les tests possibles étaient limités : Google a écrit défensivement les parties qui dialoguent avec ses bibliothèques, a exécuté ce code dans nsjail (isolation de processus légère) et a remonté les problèmes au fournisseur.
Astuce
Le plus grave danger pour une CA est le vol ou le mésusage de son matériel de clé (key material). Google garde sa clé racine hors ligne, protégée par plusieurs couches physiques exigeant chacune une autorisation à deux parties (two-party authorization) ; les clés intermédiaires, elles, restent en ligne pour l'émission quotidienne. Comme faire inclure une nouvelle CA dans tout l'écosystème prend des années, la rotation de clé après compromission n'est pas rapide : Google « mûrit » par avance d'autres matériaux racine dans l'écosystème pour pouvoir en substituer un en cas de besoin.
Côté validation des données, le système est conçu pour que la discrétion humaine ne puisse influencer ni la validation ni l'émission — on peut ainsi concentrer l'attention sur la correction du code. Des linters vérifient les certificats à plusieurs étapes (durée de vie valide, longueur du subject:commonName…), avant inscription dans des journaux de Certificate Transparency ; deux systèmes de journalisation indépendants, signés et réconciliés entrée par entrée, forment l'ultime garde-fou contre une émission malveillante. Tout cela préfigure les deux chapitres suivants : du code rendu sûr par construction, puis vérifié sans relâche.
Écrire du code : des frameworks qui imposent la sûreté
La première étape pour réduire les problèmes de sécurité et de fiabilité reste de former les développeurs — mais elle ne suffit pas. Les meilleurs experts se trompent : un spécialiste sécurité peut écrire du code vulnérable, un SRE manquer un problème de fiabilité. On peut faire relire le code par des experts, mais la revue manuelle ne trouve pas tout et le relecteur est biaisé : traquer une faille cryptographique exotique excite davantage que vérifier des centaines de gabarits HTML contre le XSS.
La parade est structurelle : traiter la sécurité et la fiabilité dans des frameworks, langages et bibliothèques communs. Idéalement, une bibliothèque n'expose qu'une interface qui rend impossible l'écriture de code porteur des classes courantes de vulnérabilités. C'est l'idée maîtresse du chapitre : intégrer la sûreté dans l'outillage de base pour que le chemin de moindre effort soit aussi le plus sûr. Quand un expert corrige un problème, il le supprime de toutes les applications qui s'appuient sur le framework : l'effort passe à l'échelle, là où la revue manuelle ne le fait pas.
Au-delà de la sécurité, les frameworks mutualisent les briques communes : authentification, autorisation, journalisation, chiffrement côté sécurité ; limitation de débit (rate limiting), répartition de charge, logique de réessai (retry) côté fiabilité. Plutôt que de réinventer ces briques pour chaque service — et d'y semer des bogues différents à chaque fois —, le développeur ne personnalise que ce qui lui est propre. Il en résulte plus de productivité, des mises à jour propagées en un seul endroit, et un code où la logique métier est nettement séparée du commun, donc plus facile à raisonner.
À retenir
Il ne faut pas toujours écrire son propre framework. Tout professionnel de la sécurité vous déconseillera de concevoir votre propre framework cryptographique : mieux vaut réutiliser une solution éprouvée comme Tink. Avant d'adopter un framework, évaluez sa posture de sécurité, préférez ceux qui sont activement maintenus, et tenez vos dépendances à jour pour bénéficier des correctifs.
Un framework de backend RPC
La plupart des backends RPC partagent une même structure : journalisation, authentification, autorisation, limitation de débit, autour d'une logique propre à la requête. Le livre propose un framework fondé sur des intercepteurs (interceptors) prédéfinis, chacun responsable d'une étape. Chaque intercepteur définit une action avant et après la logique RPC ; si une étape signale une erreur, les intercepteurs suivants ne s'exécutent pas, mais les étapes après de ceux déjà appelés se déroulent en ordre inverse. Les intercepteurs partagent un objet de contexte (context) qu'ils se transmettent.
Requête ──►[ log ]──►[ authn ]──►[ authz ]──►[ throttle ]──► logique RPC
▲ ▲ │ refus
│ │ ▼
(après) ◄──(après)◄── erreur « permission denied »
│
▼
Réponse au client (erreur correctement journalisée) Si la requête n'est pas autorisée, la logique RPC ne s'exécute pas, mais l'erreur est correctement journalisée par l'étape après de l'intercepteur de log. Le framework peut transparenter d'autres actions entre les intercepteurs (export des taux d'erreur, métriques de performance), suivre le temps d'exécution via le contexte et annuler la requête s'il devient évident qu'elle ne tiendra pas son délai. Il gère aussi les dépendances (souples ou strictes), surveille leur disponibilité, et fournit un réessai à repli exponentiel (exponential backoff) prêt à l'emploi — évitant au développeur de réimplémenter une logique dont la moindre erreur peut déclencher une défaillance en cascade (cascading failure). Voici, côté développeur, à quoi ressemble un intercepteur d'autorisation (translittéré en TypeScript) :
interface Interceptor {
before(ctx: Context, req: Request): [Context, Error | null];
after(ctx: Context, resp: Response): Error | null;
}
class AuthzInterceptor implements Interceptor {
constructor(private allowedRoles: Map<string, boolean>) {}
before(ctx: Context, req: Request): [Context, Error | null] {
// callInfo a été rempli par le framework.
const callInfo = fromContext(ctx);
if (this.allowedRoles.get(callInfo.user)) return [ctx, null];
return [ctx, new Error("Requête non autorisée de " + callInfo.user)];
}
after(_ctx: Context, _resp: Response): Error | null {
return null; // Rien à faire après le traitement du RPC.
}
} Neutraliser les vulnérabilités courantes par construction
Dans les grandes bases de code, une poignée de classes concentre la majorité des vulnérabilités. Le tableau ci-dessous reprend le top 10 OWASP et la mesure de durcissement au niveau du framework que propose le livre.
| Risque OWASP | Mesure de durcissement par le framework |
|---|---|
| Injection (SQL) | Type TrustedSqlString (voir ci-dessous). |
| Authentification défaillante | Imposer un mécanisme éprouvé (OAuth) avant de router la requête. |
| Exposition de données sensibles | Types distincts (et non string) pour numéros de carte, etc. ; protection en transit (HTTPS) ; clés chargées depuis un KMS via Tink. |
| Entités externes XML (XXE) | Parseur XML avec entités externes désactivées. |
| Contrôle d'accès défaillant | Framework exigeant que chaque handler/RPC ait des restrictions d'accès bien définies ; passer les identifiants de l'utilisateur final au backend. |
| Mauvaise configuration de sécurité | Pile à configuration sûre par défaut ; un drapeau unique pour le mode debug, jamais activé en production. |
| Cross-site scripting (XSS) | Système de gabarits durci (type SafeHtml). |
| Désérialisation non sûre | Bibliothèques conçues pour l'entrée non fiable (Protocol Buffers). |
| Composants à vulnérabilités connues | Choisir des bibliothèques populaires, activement maintenues. |
| Journalisation et supervision insuffisantes | Journaliser et superviser dans une bibliothèque de bas niveau (cf. l'intercepteur de log). |
Le cas de l'injection SQL (en tête des listes OWASP et SANS) illustre la philosophie « par construction ». Concaténer une entrée utilisateur dans une requête ("... WHERE reset_token = '" + token + "'") permet à un attaquant d'injecter des commandes. Les requêtes paramétrées corrigent le tir, mais imposer par simple consigne d'y recourir ne passe pas à l'échelle : il faudrait former chaque développeur et relire tout le code. La solution est de concevoir l'API pour que mélanger entrée utilisateur et SQL devienne impossible. On introduit un type TrustedSqlString dont, par construction, le contenu ne peut provenir que de littéraux de chaîne présents dans le code source — jamais de l'utilisateur. Le mécanisme d'enforcement dépend du langage : alias de type privé au paquet (Go), annotation @CompileTimeConstant via Error Prone (Java), constructeur par template (C++). Les rares cas légitimes (un outil d'analytique exécutant du SQL arbitraire fourni par le propriétaire des données) passent par un paquet unsafequery soumis à l'approbation d'un ingénieur sécurité — une charge de revue qu'un seul ingénieur à temps partiel, en rotation, absorbe pour des milliers de développeurs.
Note
La même approche par les types attaque le XSS. Au lieu de manipuler des chaînes brutes, on introduit SafeHtml (contenu d'un élément HTML) et SafeUrl (URL sûre à suivre), enveloppes immuables dont les contrats sont garantis par les constructeurs. Le système de gabarits à échappement contextuel strict analyse le HTML, détermine le contexte de chaque point d'insertion et exige le type correct ou échappe les chaînes non fiables. L'effet mesuré est spectaculaire : les applications d'un framework web interne développé d'emblée avec des types sûrs ont signalé deux ordres de grandeur moins de vulnérabilités XSS que les autres, malgré une revue de code soignée dans les deux cas.
Pour faire respecter ces contrats à grande échelle, mieux vaut une erreur de compilation qu'un linter optionnel ou une remarque en revue : le retour est immédiat et actionnable, le développeur a tout le contexte en tête, et corriger un type au moment où on écrit le code n'est guère plus pénible que corriger une faute de frappe. Des greffons comme Error Prone (Java) ou Tsetse (TypeScript) interdisent les motifs risqués, parfois avec des corrections automatiques proposées comme point de départ. Le déploiement sur du code existant se fait incrémentalement : on exempte d'abord le code hérité, on restreint l'API non sûre aux appelants actuels (annotation @RestrictedApi, visibilité Bazel), et la base migre organiquement vers l'API sûre.
La simplicité mène au code sûr et fiable
Un code simple offre moins d'occasions de se tromper. Le livre rappelle quelques anti-patterns. L'imbrication multiniveau (multilevel nesting) rend les bogues difficiles à repérer, surtout dans les chemins de gestion d'erreur que les tests unitaires couvrent mal — deux messages d'erreur intervertis (« mauvais encodage » vs « non autorisé ») sautent aux yeux dès qu'on refactorise pour traiter chaque erreur au plus tôt. Le principe YAGNI (You Aren't Gonna Need It) proscrit le code écrit « au cas où » : une méthode Sleep(hibernate) qui ne sera jamais appelée qu'avec false, ou un statut de retour toujours OK, ajoutent une complexité à documenter, tester et maintenir pour rien. La dette technique signalée par des TODO/FIXME n'est pas un mal en soi, à condition d'avoir un processus (et du temps alloué) pour la rembourser : tableaux de bord de santé du code (couverture, complexité cyclomatique), linters détectant code mort et dépendances inutiles, semaines de fixit régulières.
Piège courant
Le refactoring est le moyen le plus efficace de garder une base propre, mais il obéit à une règle d'or : ne jamais mêler refactoring et changements fonctionnels dans un même commit. Les changements de refactoring sont volumineux et difficiles à comprendre ; un changement fonctionnel glissé au milieu augmente fortement le risque qu'auteur et relecteur laissent passer un bogue. La première étape d'un refactoring sûr est de mesurer et augmenter la couverture de tests — sachant que 100 % de couverture ne garantit rien si les tests ne sont pas pertinents, d'où l'intérêt du fuzzing.
Choisir les bons outils
Le choix d'un langage a un impact énorme sur la sûreté. Microsoft estimait en 2019 qu'environ 70 % de toutes les vulnérabilités proviennent de problèmes de sûreté mémoire — un chiffre stable depuis plus de douze ans ; Google rapportait 85 % des bogues d'Android dus à des erreurs de gestion mémoire. La conclusion s'impose : adopter des langages à sûreté mémoire. Le tableau suivant résume l'arbitrage entre familles de langages tel que le présente le livre — un point que l'on explique en prose et en tableau plutôt qu'en bloc de code, car les détails sont spécifiques à chaque langage.
| Aspect | C / C++ (non sûr en mémoire) | Java / Go (gestion mémoire de plus haut niveau) |
|---|---|---|
| Classe d'erreurs mémoire | Débordement de tampon, use-after-free, lecture non initialisée, fuites | Éliminée par défaut (le ramasse-miettes et les bornes gèrent la mémoire) |
| Contrepartie | Contrôle bas niveau, performance, interopérabilité | Surcoût d'exécution, moins de contrôle fin |
| Filet de sécurité | Sanitizers (ASan, MSan, TSan…), Valgrind | Détecteur de course de données (Go Race Detector) |
| Conséquence | À confiner en zone sûre, à valider en amont | À privilégier pour les frontières non fiables |
Au-delà de la mémoire, le livre recommande le typage fort et la vérification statique de type : ils éliminent toute une gamme d'erreurs à la compilation plutôt qu'à l'exécution, ce qui est crucial sur de grandes bases multi-développeurs. Dans un langage à typage dynamique (Python) ou faible, on ne peut presque rien inférer sans 100 % de couverture — et JavaScript réserve des surprises ([9, 8, 10].sort() rend [10, 8, 9], car les littéraux sont traités comme des chaînes). D'où la recommandation d'extensions comme Pytype pour Python et TypeScript pour JavaScript, ajoutables incrémentalement.
Le livre insiste enfin sur l'usage de types forts plutôt que de primitives nues. AddUserToGroup(string, string) ne dit pas quel argument est le groupe ; Circle(double) attend-il un rayon ou un diamètre ? Les conversions implicites tronquent ou perdent en précision, et la confusion d'unités a coûté cher — le « Gimli Glider » d'Air Canada (carburant en livres au lieu de kilos) et la perte du Mars Climate Orbiter à 125 millions de dollars (impérial vs métrique). Des types comme User, Group, Radius, Timestamp ou Duration encapsulent l'unité, n'autorisent que les opérations sensées et rendent le code auto-documenté. Là où le langage n'élimine pas les pièges mémoire, on assainit le code (sanitizing) à la soumission ou dans la chaîne d'intégration continue : en C++, Valgrind (qui interprète le binaire sans recompilation) ou la suite Google Sanitizers — ASan (erreurs mémoire), LSan (fuites), MSan (mémoire non initialisée), TSan (courses et interblocages), UBSan (comportement indéfini) —, cette dernière jusqu'à dix fois plus rapide ; en Go, le Race Detector.
Tester le code : prouver la sûreté
Aussi soigneux que soient les ingénieurs, des erreurs et des cas limites oubliés sont inévitables — combinaisons d'entrées corrompant les données, « requête de la mort » (Query of Death) saturant un service, débordements de tampon. Les techniques de test ont des profils coût/bénéfice variés et se complètent ; aucune ne remplace les autres. Le tableau ci-dessous synthétise ce que chacune trouve.
| Technique | Principe | Ce qu'elle trouve | Coût / portée |
|---|---|---|---|
| Test unitaire | Exercer une « unité » isolée avec des entrées choisies | Régressions, cas limites prévus, mauvaise gestion d'entrées malveillantes | Rapide, local, avant chaque commit |
| Test d'intégration | Remplacer les doublures par les vraies dépendances | Erreurs aux interactions entre composants | Plus lent, plus instable (latence réseau) |
| Analyse dynamique | Instrumenter et exécuter le binaire | Corruption mémoire, comportement indéfini, fuites, courses | Lent (binaire instrumenté), souvent nocturne |
| Fuzzing | Générer en masse des entrées et observer les plantages | Corruption mémoire, exceptions à l'exécution, DoS, fuites (ex. Heartbleed) | Continu, complémentaire |
| Analyse statique | Inspecter le code source sans l'exécuter | Motifs douteux, code mort, bogues probables, propriétés formelles | Du linter rapide aux méthodes formelles coûteuses |
Tests unitaires et tests d'intégration
Le test unitaire découpe le logiciel en unités autonomes, sans dépendance externe, et exerce chacune avec des entrées choisies. Les frameworks de la famille xUnit (JUnit, GoogleTest, le module unittest de Python) standardisent installation, démontage et format de résultat. De bons tests sont rapides, fiables et hermétiques : un test qui ne reproduit pas le même résultat en isolation n'est pas fiable. Pour un système de quotas de stockage, un test orienté sécurité vérifiera le traitement des montants négatifs, des débordements de capacité, ou d'une entrée malformée. On écrit souvent les tests juste après le code (ou avant, en TDD, test-driven development), et la revue par les pairs veille à leur robustesse : un relecteur qui peut remplacer une condition par if (false) ou if (true) sans faire échouer un seul test découvre des cas oubliés — c'est l'idée du test de mutation (mutation testing) automatisé chez Google.
Rendre le code testable suppose parfois de le refactorer pour intercepter les appels aux systèmes externes : remplacer l'appel direct à un traqueur de tickets par une interface IssueTrackerService dont une implémentation de test enregistre les appels. Ces doublures s'appellent mocks, stubs ou fakes — concepts distincts dont l'organisation doit aligner le vocabulaire. Attention à la sur-abstraction : des tests qui n'affirment que l'ordre des appels « testent » le flot de contrôle du langage, pas le comportement qui compte, et exigent une réécriture à chaque changement de méthode.
Le test d'intégration va plus loin en remplaçant les doublures par les vraies implémentations (base de données, services réseau), exerçant des chemins plus complets au prix de plus de lenteur et d'instabilité. Une bonne journalisation aux jonctions logiques aide à comprendre pourquoi un test d'intégration échoue alors que tous les unitaires passent.
Attention
Tentant mais dangereux : refléter de vraies bases de données dans les environnements de test. Elles peuvent contenir des données sensibles accessibles à quiconque lance les tests — une violation du moindre privilège (least privilege). Mieux vaut semer ces systèmes avec des données de test non sensibles, ce qui permet en outre de remettre l'environnement à un état propre connu et réduit l'instabilité.
Fuzzing : l'arme contre les entrées inattendues
Le fuzzing (fuzz testing) complète les autres stratégies : un moteur de fuzzing (fuzzer) génère un grand nombre d'entrées candidates qu'un pilote (fuzz driver) passe à la cible (fuzz target), puis analyse comment le système réagit. Les cibles de choix sont les codes traitant des entrées complexes : parseurs de fichiers, algorithmes de compression, protocoles réseau, codecs audio.
Astuce
Le fuzzing trouve la corruption mémoire à implication sécuritaire, mais aussi les exceptions à l'exécution pouvant causer un déni de service en cascade en Java ou Go, et il sert à comparer deux implémentations d'une même fonction (tout résultat divergent est signalé comme « plantage »). Sa puissance éclate combinée aux sanitizers : avec le bon pilote, le célèbre bogue Heartbleed (CVE-2014-0160), qui faisait fuiter la mémoire des serveurs web, est identifié relativement vite — ASan pointe le memcpy exact qui lit au-delà du tampon alloué.
Les moteurs vont du dumb fuzzing (octets purement aléatoires) aux moteurs guidés par la couverture qui, grâce à l'instrumentation du compilateur, conservent les échantillons atteignant un nouveau chemin de code pour en dériver de meilleurs. On les nourrit de dictionnaires de mots-clés (HTTP, SQL, JSON) et d'un corpus de départ (seed corpus) de fichiers représentatifs, qu'ils mutent. Comme le fuzzing peut tourner indéfiniment, on ne bloque pas chaque commit dessus : un bon pilote est déterministe, évite les opérations lentes (I/O disque, logs) et ne plante jamais intentionnellement — car le moteur ne distingue pas un plantage voulu d'un bogue. Des moteurs comme libFuzzer, AFL, Honggfuzz partagent souvent le même point d'entrée, et le fuzzing continu transforme tout cela en boucle de rétroaction : ClusterFuzz orchestre des pools de machines, déduplique les plantages et reteste les anciens ; OSS-Fuzz, son extension distribuée sur Google Cloud, avait découvert plus de mille bogues dans les cinq mois suivant son lancement de décembre 2016, et des dizaines de milliers depuis.
Analyse statique : du linter aux méthodes formelles
L'analyse statique examine et comprend un programme sans l'exécuter, en bâtissant une représentation interne de son code source. Une limite fondamentale la borne : vérifier statiquement une propriété arbitraire d'un programme est indécidable — d'où des compromis inévitables entre faux positifs (avertissements erronés) et faux négatifs (avertissements manqués). Les outils se répartissent sur un spectre de profondeur croissante.
superficiel ◄──────────────────────► profond
│ │ │ │
Linters Interprétation Méthodes (coût, précision,
(motifs AST) abstraite formelles temps d'analyse ↑) Les outils d'inspection automatisée (linters) font une analyse syntaxique — souvent par filtrage de motifs sur l'arbre syntaxique abstrait (AST) — sans modéliser les flux complexes ; ils passent à l'échelle (durée proche d'une compilation) et sont extensibles. Error Prone (Java) et Clang-Tidy (C/C++) sont massivement utilisés chez Google et acceptent des règles personnalisées (162 auteurs avaient soumis 733 vérifications à Error Prone début 2018) ; certains proposent des corrections automatiques (--fix), par exemple suggérer absl::StartsWith à la place d'un s.find(...) == 0, repérer un sizeof sur un pointeur, ou moderniser NULL/0 en nullptr. GoVet (Go) et Pylint (Python) jouent un rôle analogue. Ces outils gagnent à être intégrés tôt dans le cycle, le coût d'un bogue grimpant fortement une fois le code poussé ou déployé ; la plateforme Tricorder de Google (et son équivalent open source Shipshape) analyse environ 50 000 changements par jour avec 146 analyseurs sur plus de 30 langages, en visant un taux de faux positifs perçu inférieur à 10 %.
Plus en profondeur, l'interprétation abstraite (abstract interpretation) effectue une analyse sémantique des comportements en raisonnant sur le flux de données et de contrôle, souvent à travers les appels de fonction. Elle résume les valeurs possibles par une représentation compacte — par exemple, les dix plus petits entiers pairs positifs deviennent l'intervalle [2, 20] — au risque d'imprécision (un faux positif si l'on voulait prouver que la valeur 11 n'apparaît jamais). Plus lente, elle tourne plutôt la nuit ou en revue différentielle ; des outils comme Frama-C (erreurs d'exécution en C), Infer (pointeurs pendants en Java/C) ou le programme App Security Improvement de Google (qui a mené à plus d'un million de corrections d'applications sur le Play Store) en relèvent. Enfin, les méthodes formelles (formal methods) laissent spécifier mathématiquement des propriétés de sûreté (un mauvais comportement ne doit jamais survenir) ou de vivacité (un résultat souhaité finit par survenir) et les vérifier — voire développer des systèmes corrects par construction. Leur coût initial élevé les réserve à des domaines spécialisés : conception matérielle, systèmes critiques, ou vérification continue des protocoles cryptographiques de TLS.
À retenir
- Sécurité et fiabilité ne se greffent pas après coup : elles se conçoivent dès le départ, on commence petit et l'on itère — la CA de Google a mûri sur près d'une décennie, mais sur de bons principes posés d'emblée.
- Le code sûr par construction l'emporte sur la vigilance : des frameworks et bibliothèques durcis (types
TrustedSqlString,SafeHtml, intercepteurs RPC, Tink) exposent une interface où le chemin de moindre effort est aussi le plus sûr et corrigent une vulnérabilité en un seul endroit. - Choisir les bons outils a un impact énorme : ~70 % des vulnérabilités viennent de la mémoire, d'où les langages à sûreté mémoire (Go), le typage fort, les types forts contre la confusion d'unités, et les sanitizers (ASan, TSan…) ou Valgrind là où le C++ s'impose.
- La simplicité est défensive : segmentation et zones de confiance limitent ce qu'un attaquant peut affecter ; éviter l'imbrication, appliquer YAGNI, rembourser la dette et refactorer (jamais mêlé à un changement fonctionnel) réduisent les bogues.
- Aucune technique de test ne remplace les autres : tests unitaires et d'intégration, analyse dynamique, fuzzing et analyse statique ont des profils coût/bénéfice distincts et se complètent.
- Le fuzzing déniche l'inattendu : combiné aux sanitizers, il révèle corruptions mémoire et DoS (Heartbleed identifié relativement vite) ; rendu continu via ClusterFuzz et OSS-Fuzz, il devient une boucle de rétroaction permanente.
- Intégrez tout dans le cycle et la CI/CD : analyse statique au plus tôt (Tricorder, < 10 % de faux positifs perçus), tests hermétiques sans données sensibles, et défense en profondeur (parseurs Go et C++ comparés) pour détecter les bogues vite et tôt.