J'ai audité mon propre projet open-source avec 26 agents IA (et trouvé une vraie faille)

ShareBox, c'est mon serveur de streaming auto-hébergé : un truc en PHP que j'ai construit parce que je voulais juste envoyer un lien de film à quelqu'un sans installer Plex et ses dix gigas de dépendances. Il tourne sur ma seedbox, sert mes utilisateurs, et un matin je remarque qu'il commence à prendre quelques étoiles sur GitHub.

Et là, cette petite voix : « est-ce que ça tient vraiment la route, ce truc ? » Parce que entre « ça marche chez moi » et « du code que des inconnus vont installer sur leur machine », il y a un gouffre. Un gouffre rempli de failles que je ne vois plus, parce que je suis le nez dedans depuis des semaines.

Normalement, on relit son code. Sauf que relire 22 000 lignes seul, honnêtement, on le fait mal : on survole ce qu'on croit connaître. Alors j'ai tenté autre chose — lâcher une meute de 26 agents IA dessus, chacun avec une mission précise, et voir ce qui remonte. Spoiler : ils ont trouvé une faille que j'avais sous les yeux depuis le début.

26 agents pour éplucher mon propre code

L'idée n'était pas « IA, dis-moi si mon code est bien » — ça donne toujours la même bouillie encourageante et inutile. L'idée, c'était d'orchestrer : découper l'audit en rôles, faire travailler les agents en parallèle, puis faire critiquer les conclusions par un dernier agent volontairement vache.

Le pipeline ressemblait à ça : onze lecteurs partent en parallèle, chacun avale un pan entier du code (le cœur, les handlers de streaming, l'API, le front, les tests, le Docker…). Leurs comptes-rendus remontent vers une synthèse d'architecture et une analyse de couverture de tests. Ensuite, douze agents « radar » notent chacun un seul axe — sécurité, perf, archi, tests… Et pour finir, un agent « verdict » relit toutes les notes en mode adversarial : son job, c'est de descendre celles qui sont trop gentilles.

Pipeline de l'audit : 11 lecteurs en parallèle, puis synthèse, puis 12 agents radar, puis un verdict adversarial. 11 lecteurs en parallèle chaque pan du code lu en entier Synthèse archi + couverture on relie les morceaux, on mesure les trous 12 agents radar un agent = un seul axe noté Verdict adversarial descend les notes trop gentilles → note finale + feuille de route
Le pipeline : lire en parallèle, relier, noter axe par axe, puis tout faire critiquer par un dernier agent volontairement sévère.

Le verdict est tombé : 5,04 sur 10. Calibrage volontairement dur — l'agent final avait pour consigne de noter comme un staff engineer exigeant, avec en tête que « un media-server PHP de quelques semaines n'est pas Jellyfin ». Ça pique un peu sur le moment. Mais une note basse et argumentée vaut mille fois mieux qu'un « super projet ! » de complaisance.

La faille que j'avais sous les yeux

Le moment qui justifie à lui seul tout l'exercice : un des agents sécurité tique sur le script de démarrage Docker. Mon entrypoint.sh génère le config.php à partir de variables d'environnement. Et il y avait, juste au-dessus, un commentaire de ma main : « Sanitize strings to prevent PHP injection ».

Sauf que la sanitisation ne couvrait que les chaînes. Trois variables numériques/booléennes, elles, étaient interpolées brutes dans le fichier PHP généré :

define('STREAM_MAX_CONCURRENT', ${MAX_CONCURRENT});

Traduction : si quelqu'un déploie le conteneur avec une variable d'environnement du genre :

SHAREBOX_STREAM_MAX_CONCURRENT='1);system($_GET[x]);//'

…alors le config.php généré contient du PHP exécutable. Une exécution de code arbitraire, dans le fichier de config, sous le nez d'un commentaire qui prétendait l'empêcher. Le genre de truc qu'on ne voit plus parce qu'on l'a écrit soi-même et qu'on lui fait confiance.

Attention — toujours vérifier les agents. Avant de croire l'agent sur parole, je suis allé relire le source moi-même. C'est la règle d'or : un agent qui dit « faille confirmée » peut se tromper, et un agent qui dit « tout va bien » aussi. Ici la faille était réelle. Le correctif : valider que ces variables sont bien des entiers (ou true|false) avant de les écrire, sinon repli sur une valeur sûre.

Au passage, l'audit a aussi pointé des endpoints d'écriture (gestion des posters TMDB) accessibles à n'importe quel porteur d'un lien public, un export ZIP non borné qui pouvait monopoliser le serveur, et une sauvegarde de base de données déclenchée à chaque requête web. Sept correctifs « quick win » au total — chacun vérifié dans un vrai conteneur Docker avec une suite de tests de bout en bout avant de toucher à la prod.

Quand mon propre correctif a introduit un bug

Voilà le moment le plus instructif, et le plus humble. Après avoir appliqué mes sept correctifs, j'ai relancé un cycle d'agents — cette fois pour re-noter et chasser les régressions. Et l'un d'eux a trouvé un bug. Dans mon correctif.

En sortant la sauvegarde de base de la requête web (bonne idée), je l'avais branchée sur le démarrage du conteneur. Or ce code tourne en root, et SQLite en mode WAL crée des fichiers annexes (-wal, -shm). Résultat : au redémarrage sur un volume déjà peuplé, ces fichiers appartenaient à root, et le serveur web (qui tourne en www-data) ne pouvait plus écrire dans la base. Un correctif qui marche au premier lancement et casse au second. Le pire type de bug.

Aucun humain pressé n'aurait relancé un audit complet juste après avoir « fini ». C'est précisément là que la meute d'agents gagne : elle ne se fatigue pas, elle ne se félicite pas, elle relit le diff avec la même rigueur froide la deuxième fois que la première. Bug corrigé, re-testé au redémarrage, et cette fois pour de bon.

Des tests qui lisent le code vs des tests qui l'exécutent

L'autre claque de l'audit a porté sur mes tests. J'en avais des centaines, j'étais fier de mon petit badge vert. Sauf qu'un agent a mis le doigt sur un mensonge confortable : une grande partie de ces tests lisaient le code source et vérifiaient qu'une chaîne de caractères y était présente — au lieu d'exécuter le code et de vérifier son comportement.

// Ce que faisait le test (lecture du source) :
$source = file_get_contents('functions.php');
$this->assertStringContainsString('aresample=async=1', $source);

// Ce qu'un vrai test fait (exécution) :
$args = buildFilterGraph(720, 0, burnSub: 2);
$this->assertStringContainsString('overlay', $args); // le filtre est VRAIMENT construit

La différence est énorme. Le premier test reste vert même si la fonction est cassée, tant que la chaîne traîne quelque part dans le fichier. C'est une couverture en trompe-l'œil. Le code qui sert réellement les octets d'un fichier (la gestion des Range HTTP, le cœur d'un serveur de streaming) n'était jamais exécuté en test.

J'ai donc converti les chemins critiques en vrais tests d'exécution : un serveur PHP éphémère qui sert un fichier et vérifie les réponses 206 Partial Content octet par octet, les vrais appels aux constructeurs de commandes ffmpeg, le gate de sécurité testé en HTTP réel. L'axe « couverture E2E » est passé de 3 à 5 sur le radar — le plus gros bond de tout l'exercice, et le plus mérité.

Le meilleur conseil de l'audit : ne pas le suivre jusqu'au bout

À la fin, la note était remontée de 5,04 à 5,72. Et l'envie naturelle, c'est de continuer à grimper. Le verdict pointait clairement le plafond : deux fichiers monolithiques de ~2 300 lignes chacun, mêlant routage, auth, métier et vues. La « bonne » réponse d'école : tout découper, introduire un routeur, des contrôleurs, des namespaces.

Et là, l'agent final a fait quelque chose que je n'attendais pas d'un audit : il m'a déconseillé de le faire.

Un bon audit dit aussi ce qu'il ne faut PAS faire. Réécrire 4 700 lignes de code procédural qui fonctionnent, dont le cœur (le pipeline ffmpeg) n'est même pas couvert en exécution, pour un projet open-source solo : des semaines de travail, un risque de régression énorme, et zéro valeur ajoutée pour l'utilisateur. Le ratio risque/valeur est mauvais. Garder une architecture procédurale assumée est un choix défendable.

À la place, le geste à plus fort levier était invisible et sans risque : étendre l'analyse statique (PHPStan) à ces gros fichiers jamais analysés, en figeant la dette existante dans un baseline. Résultat : ~4 700 lignes parmi les plus risquées sont désormais sous filet — toute nouvelle régression bloque la CI, sans m'imposer de tout nettoyer aujourd'hui. Cinq minutes de config valaient mieux que trois semaines de refonte.

Ce qu'il faut retenir

La meute d'agents n'a pas « audité à ma place ». Elle a fait ce qu'un humain seul fait mal : lire vraiment chaque ligne, sans survol, sans angle mort de l'auteur, en parallèle, et recommencer après les correctifs sans se lasser. Elle a trouvé une faille réelle, démasqué une couverture de tests en trompe-l'œil, et même attrapé un bug dans mon propre correctif.

Mais à aucun moment elle n'a décidé à ma place. C'est moi qui ai vérifié la faille dans le source avant d'y croire. C'est moi qui ai tranché qu'un correctif méritait un déploiement et un autre une attente. Et c'est l'agent adversarial, pas l'agent complaisant, qui a produit la vraie valeur — en notant sévèrement, et en disant « stop, n'en fais pas trop ».

Le vrai gain n'est pas la note qui monte. C'est de savoir, chiffres à l'appui, où est la dette, laquelle vaut le coup d'être remboursée, et — le plus dur pour un dev — laquelle il faut accepter de laisser tranquille.

Commentaires (0)