Failles silencieuses : quand le code IA marche et te trahit

Une « faille silencieuse », c'est du code qui passe tous tes tests, tourne en prod sans broncher... et t'expose quand même. La différence avec un bug est simple : un bug casse, tu le vois tout de suite. Une vulnérabilité logique, elle, marche parfaitement — c'est exactement pour ça qu'elle est dangereuse. Rien ne clignote en rouge.

L'IA en produit beaucoup, pour une raison mécanique : elle reproduit les exemples simplifiés de son corpus d'entraînement, ceux des tutoriels, sans les protections que ces tutoriels omettent pour rester lisibles. Le code « fonctionne » donc parfaitement, mais il lui manque la couche de sécurité que personne n'a écrite.

Le réflexe qui te protège tient en une phrase : sur les zones sensibles — authentification, paiement, upload de fichiers, réinitialisation de mot de passe — tu ne te fies jamais au simple fait que « ça marche ». Tu relis, tu scannes, et tu demandes à l'IA d'attaquer son propre code. La suite te montre comment, avec un exemple bien réel.

Bug ou vulnérabilité : pourquoi l'un se voit et l'autre non

Un bug, c'est un comportement cassé. Le bouton ne répond pas, la page plante, le total est faux. Tes tests le rattrapent, ton utilisateur râle, tu corriges. Le signal est immédiat.

Une vulnérabilité logique, c'est l'inverse. Le code fait exactement ce qu'on lui demande, dans le cas normal. Il n'échoue que face à un usage que personne n'avait prévu : une requête forgée, un temps de réponse mesuré, un champ détourné. Tant que personne ne cherche, tout va bien. C'est ce silence qui piège les vibecodeurs comme les devs pressés.

Retiens la distinction : un test vérifie que ton code fait ce qu'il doit. La sécurité vérifie qu'il ne fait que ça. Deux questions différentes.

L'exemple qui résume tout : le reset de mot de passe

Le guide « Secure Vibe Coding » publié par The Hacker News en juin 2025 donne un exemple parlant. Tu demandes à l'IA une routine de réinitialisation de mot de passe. Elle te sort un code impeccable, qui compare le jeton reçu avec le jeton stocké :

// Généré par l'IA : ça marche parfaitement
if (token === storedToken) {
  resetPassword(user);
}

Tous tes tests passent. Le bon jeton ouvre la porte, un mauvais la referme. Et pourtant, c'est troué.

Le problème : === sur des chaînes s'arrête dès le premier caractère qui diffère. Comparer "a..." prend un poil moins de temps que comparer "ab...". Ces microsecondes sont mesurables. Un attaquant envoie des milliers de requêtes, mesure les temps de réponse, et reconstitue le jeton caractère par caractère. C'est une timing attack.

La parade est une comparaison à temps constant, qui prend toujours la même durée quel que soit l'écart :

import { timingSafeEqual } from 'node:crypto';

const a = Buffer.from(token);
const b = Buffer.from(storedToken);
if (a.length === b.length && timingSafeEqual(a, b)) {
  resetPassword(user);
}

Même logique, même résultat fonctionnel. Mais le temps de réponse ne fuite plus rien. L'IA ne l'a pas écrit spontanément parce que la quasi-totalité des tutoriels utilise === : c'est plus court à lire.

Pourquoi l'IA écrit du code « propre » mais troué

L'IA n'a pas d'intention. Elle prédit la suite la plus probable, et le plus probable ressemble au tutoriel moyen. Or les tutoriels coupent volontairement les protections : la validation d'entrée, la comparaison sécurisée, la gestion des permissions. Ça alourdit l'exemple, ça n'aide pas à comprendre le concept. Résultat : l'IA a appris la version simplifiée comme si c'était la norme.

Les secrets en dur suivent la même pente. D'après le même guide « Secure Vibe Coding » de The Hacker News, les dépôts de code assistés par IA exposent environ 40 % de secrets en plus — clés d'API, identifiants écrits en dur — que les bases de code classiques. Là encore, logique : dans un exemple de tutoriel, apiKey = "sk-..." en haut du fichier, c'est plus direct qu'un montage propre avec variables d'environnement.

Ce n'est pas une fatalité. C'est juste un angle mort connu, donc anticipable.

Tes parades concrètes

Tu n'as pas besoin d'être expert en sécurité. Tu as besoin de quelques réflexes bien placés.

  • Revue humaine ciblée. Ne relis pas tout ligne par ligne. Concentre ton attention sur les quatre zones qui font mal : authentification, paiement, upload, reset. C'est 10 % du code et l'essentiel du risque.
  • Scanners dans la CI. Branche un scanner de secrets et un scanner de vulnérabilités dans ton pipeline. Ils tournent à chaque commit, sans fatigue et sans oubli. Un secret poussé par erreur est repéré avant d'atteindre la prod.
  • Fais auditer l'IA par elle-même. Reprends le code qu'elle vient d'écrire et change de casquette : « Tu es un attaquant. Comment casserais-tu ce code ? Liste les vulnérabilités et les vecteurs d'attaque. » Le même modèle qui a produit le trou sait souvent le nommer quand on le lui demande sous cet angle.
  • Secrets en variables d'environnement. Jamais de clé en dur dans le code. Un fichier .env ignoré par Git, des variables injectées à l'exécution. Si tu veux le détail, j'en parle dans protéger ses clés API.

Et si ton app repose sur Supabase, la sécurité au niveau des lignes mérite le même soin — voir sécuriser Supabase avec RLS.

Par où commencer aujourd'hui

Une checklist courte, applicable ce soir sur ton projet :

  1. Liste tes zones sensibles. Écris noir sur blanc où passent l'auth, le paiement, l'upload, le reset. Tu ne peux pas protéger ce que tu n'as pas nommé.
  2. Passe l'IA en mode attaquant sur chacune de ces zones, une par une, avec le prompt ci-dessus.
  3. Cherche les secrets en dur. Un grep sur key, secret, token, password dans ton code. Tout ce qui traîne part en variable d'environnement.
  4. Ajoute un scanner à ta CI. Même gratuit, même minimal. L'important est qu'il tourne à chaque push.
  5. Remplace tes comparaisons de jetons par une version à temps constant partout où un secret est comparé.

Aucune de ces étapes ne demande d'être expert. Elles demandent juste de traiter « ça marche » comme le début de la vérification, pas comme la fin. C'est toute la différence entre un code qui passe les tests et un code qui tient.