Retour à tous les articles
Publié le · par Renaud Deraison

Le garde et le shell n'étaient pas d'accord

Le 30 juin, Adversa AI a divulgué GuardFall — une classe de contournements qui fait passer des astuces bash vieilles de plusieurs décennies devant les gardes de commandes intégrés à 10 des 11 agents de code open source les plus populaires. La raison est simple et impossible à corriger à ce niveau : le garde lit le texte, et bash réécrit le texte avant qu'il ne s'exécute. Bromure Agentic Coding n'a jamais parié sur la capacité du garde à lire bash correctement. Il a parié sur le fait que la commande n'a pas d'importance au moment où elle s'exécute.

Le filtre de sécurité d'un agent de code a regardé la chaîne r''m -rf ~ et a vu une commande qu'il ne reconnaissait pas, alors il l'a laissée passer. Bash a regardé la même chaîne, a jeté les guillemets vides, et a exécuté rm -rf ~. Le filtre et le shell lisaient deux langages différents. Cet écart n'est pas un bug d'un outil — c'est toute l'idée de garder un agent en filtrant par motif les commandes qu'il tape, et Adversa AI vient de montrer qu'elle échoue dans dix des onze agents qu'ils ont testés.

Le garde de commandes est la fonctionnalité vers laquelle tout outil de code agentique se tourne en premier. L'agent propose une commande shell ; un filtre inspecte la chaîne ; si elle a l'air destructrice — rm -rf, un curl … | sh par tube, un dd vers un périphérique brut — le garde la bloque ou vous demande de confirmer. C'est ce qui vous permet d'activer l'exécution automatique en vous sentant en sécurité. Le 30 juin 2026, Adversa AI a publié une étude, reprise par The Hacker News et SecurityWeek, montrant que cette fonctionnalité n'est que du théâtre.

Ce qu'est GuardFall

Ils l'appellent GuardFall, et ce n'est pas une CVE. Il n'y a rien à corriger, parce que ce n'est pas une faille dans un composant — c'est une disparité entre deux d'entre eux. Selon les mots d'Adversa : « Un garde inspecte le texte brut, tandis que le shell système (bash) développe, déquote et réécrit le texte avant de l'exécuter. » Le filtre s'appuie sur la chaîne émise par le modèle. Bash fait ensuite passer cette chaîne par la suppression des guillemets, l'expansion des variables et la substitution de commandes avant qu'un seul octet ne s'exécute. Les deux ne regardent jamais la même commande, donc toute astuce qui survit à la réécriture mais pas au filtrage passe sans encombre.

Ces astuces sont plus anciennes que la plupart des gens qui utilisent ces agents. Adversa les a regroupées en cinq classes :

  • Suppression des guillemets. r''m, ce sont deux jetons pour une regex et le mot rm pour bash, qui laisse tomber les guillemets vides.
  • Expansion des variables. rm$IFS-rf$IFS/ ressemble à un long mot unique pour un filtre par motif ; bash développe le séparateur de champ interne et obtient rm -rf /.
  • Substitution de commandes. $(echo rm) -rf / cache le verbe dangereux à l'intérieur de $( ) ; le garde qui inspecte la chaîne extérieure ne voit jamais rm.
  • Pipelines encodés. echo <base64> | base64 -d | sh — chaque jeton visible est inoffensif ; la charge n'existe qu'après décodage.
  • Options destructrices alternatives. Pas de rm du tout : find /x -delete, tar -C / -x, sed -i. Une liste noire bâtie autour de rm et dd ne se déclenche jamais.

Adversa a lancé ces astuces contre onze des agents de code et d'usage d'ordinateur open source les plus utilisés — opencode, Goose, Cline, Roo-Code, Aider, Plandex, Open Interpreter, OpenHands, SWE-agent et le projet Hermes parmi eux, cumulant environ 548 000 étoiles GitHub entre eux en mai. Dix sont tombés. Le seul qui a tenu, Continue, a tenu pour une raison précise : au lieu de filtrer la chaîne brute, il tokenise la commande comme bash le fera avant de décider — en absorbant les guillemets, en résolvant $IFS, en extrayant la commande interne d'une substitution, en signalant tout tube qui finit par sh. Il modélise le shell. C'est la seule chose qui fonctionne au niveau du garde, et c'est de l'ingénierie réelle et soignée que les dix autres devront désormais reproduire, correctement, et maintenir correcte à chaque cas limite de bash et à chaque version.

L'AGENT ÉMET UNE CHAÎNEr''m -rf ~GARDE · filtre le texte brutvoit les jetonsr''m · -rf · ~pas de rm → autorisé ✓BASH · réécrit, puis exécutejette les guillemets videsrm -rf ~répertoire personnel effacé
Pourquoi le garde perd. L'agent émet une chaîne ; le garde par filtrage la lit telle qu'écrite et ne trouve rien de destructeur ; bash développe, déquote et réécrit ensuite la même chaîne en rm -rf avant que le verdict du filtre ne veuille dire quoi que ce soit. Le garde et le shell regardent deux commandes différentes — la prémisse même de GuardFall.

Le problème, c'est le niveau, pas la regex

La tentation est de lire GuardFall comme « dix équipes ont écrit des filtres faibles ». Cela passe à côté de ce que dit Adversa. Leur phrase la plus tranchante ne concerne aucun agent en particulier :

Un agent qui peut exécuter des commandes shell arbitraires sur l'hôte de l'opérateur, contrôlé par une regex qui filtre la chaîne émise par le LLM, n'est pas une défense. Il échoue alors qu'il est pleinement activé et correctement configuré, parce que le filtrage de chaîne ne peut pas modéliser ce que bash va exécuter.

Pleinement activé et correctement configuré. Ce n'est pas une mauvaise configuration que vous pouvez refermer. Le garde se tient au seul niveau où il ne peut jamais gagner — entre un modèle qui écrit des chaînes et un shell qui les réinterprète — et demande à un filtre textuel de prédire la sortie d'un développeur Turing-complet. L'approche tokenise-et-canonise de Continue réduit l'écart en apprenant au garde à penser comme bash, et c'est le bon geste si vous comptez garder un garde là. Mais c'est un engagement par agent et par version à surpasser l'analyse d'un shell qui a passé trente ans à accumuler des façons de réécrire une commande. Neuf autres agents populaires montrent combien il est facile de se tromper.

Les contrôles compensatoires d'Adversa sont révélateurs. Avant toute correction structurelle, ils vous disent de rediriger $HOME vers un bac à sable restreint qui conserve l'accès au projet mais retire les identifiants, de retirer les options d'exécution automatique, d'empêcher les agents de tourner sur des pull requests non fiables, et de traiter chaque fichier de configuration de dépôt comme du code non fiable. Relisez cette liste. Ce n'est pas un conseil sur comment écrire un meilleur filtre. C'est un conseil sur comment construire une boîte autour de l'agent pour que, quand le filtre perd — et ils vous disent qu'il perdra — la commande qui passe atterrisse quelque part où elle ne peut pas vous nuire.

Cette boîte est le produit.

Bromure n'a jamais demandé au garde de lire bash

Bromure Agentic Coding part de l'hypothèse que GuardFall démontre : l'agent va, tôt ou tard, exécuter une commande que vous n'avez pas sanctionnée. Il n'essaie pas d'attraper cette commande en la lisant. Il fait tourner l'agent entier — Aider, Goose, opencode, celui des dix que vous voulez — à l'intérieur d'une VM Linux jetable, et achemine chaque requête réseau via un proxy sur l'hôte. La question de conception n'est jamais « le filtre reconnaîtra-t-il ce rm ? ». C'est « quand le rm s'exécute, qu'atteint-il ? ».

Faites passer les charges mêmes de GuardFall par cette boîte.

Le but de l'attaque, une fois qu'un r''m caché ou un find /x -delete a glissé un README piégé devant le garde, est de voler ce que le compte peut atteindre — Adversa nomme ~/.ssh, ~/.aws, les identifiants cloud, « tout ce qui se trouve dans votre dossier personnel » — et de l'effacer ou de l'exfiltrer. Dans Bromure, ce sont les choses qui n'ont jamais été dans la boîte. Les vrais secrets n'entrent jamais dans la VM : quand vous donnez à un profil une AWS_SECRET_ACCESS_KEY, un jeton GitHub, une clé Anthropic, l'environnement de l'agent reçoit un substitut brm_…, et le proxy de l'hôte échange le faux contre la vraie valeur sur le fil, uniquement lors de la requête sortante vers le fournisseur qui doit la recevoir. La mitigation recommandée par Adversa — déplacer $HOME quelque part qui « conserve l'accès au projet mais retire les identifiants » — est une version artisanale et partielle de ce qu'un profil Bromure fait par défaut, pour chaque identifiant, sans que vous ayez à le scripter.

SANS — vrai home, vraies clésla charge GuardFall s'exécutecat ~/.aws/credentials wJalrXUtn…cat ~/.ssh/id_ed25519 -----BEGIN…l'attaquant a des clés validesvrais comptes, vrai impactexfiltreAVEC BROMURE — boîte jetable, leurresmême charge (dans la VM)cat ~/.aws/credentials brm_d4e5f6…cat ~/.ssh/id_ed25519 brm_a1b2c3…PROXY HÔTE — les vraies clés n'entrent jamais dans la VM ; injectées seulement pour la vraie requête fournisseurl'attaquant a des leurres brm_…n'authentifient rienle home est un clone jetableexfil journalisée · reset l'efface
La charge de GuardFall passe dans les deux mondes — Bromure n'empêche pas la commande de s'exécuter. À gauche : sur un hôte normal, la commande injectée lit le vrai matériel ~/.ssh et ~/.aws et l'expédie ; ça marche. À droite : dans Bromure la même commande s'exécute, mais le répertoire personnel est un clone jetable, les identifiants qu'elle trouve sont des leurres brm_… les vraies clés vivent sur l'hôte, et la tentative d'exfiltration est une ligne enregistrée dans le Journal de sécurité. La commande s'est exécutée et n'a rien atteint.

La moitié destructrice atterrit de la même manière. Un find /x -delete ou un sed -i qui glisse le garde s'exécute bel et bien — Bromure ne prétend pas avoir attrapé la commande. Il s'exécute contre un répertoire personnel cloné depuis une base partagée, sur une VM que vous pouvez effacer et réinitialiser à la base depuis un menu, pas un formulaire d'incident. La persistance qu'une charge pourrait déposer meurt à cette réinitialisation. Et parce que chaque requête part via le proxy de l'hôte, la balise curl … | base64 -d que la classe des pipelines encodés de GuardFall est faite pour cacher n'est pas invisible : c'est une ligne enregistrée et attribuable dans le Journal de sécurité.

Le seul endroit où le fil bat le shell

Bromure conserve bien un garde des opérations destructrices — ses Guardrails côté hôte, dont nous avons parlé après qu'un agent Cursor a supprimé une base de données de production en neuf secondes. Et ici le niveau joue en faveur de Bromure, parce que les Guardrails ne vivent pas là où vit GuardFall.

GuardFall est une attaque de texte shell. Chacune de ses cinq classes — suppression des guillemets, $IFS, $( ), pipelines base64, options exotiques — est une astuce de la façon dont bash analyse une chaîne. Les Guardrails de Bromure ne lisent jamais cette chaîne. Ils se tiennent sur le proxy de l'hôte et classifient l'appel d'API structuré que l'agent fait à un fournisseur qu'il comprend — la vraie requête HTTPS vers AWS, Kubernetes, une forge git, une base de données managée — et renvoient un 403 ferme sur les appels destructeurs, sur le fil, là où un agent compromis dans la VM ne peut pas les désactiver. Au moment où une requête atteint ce niveau, c'est déjà un DeleteDBInstance analysé, pas une chaîne de guillemets et de dollars. Il n'y a aucune réécriture de shell pour faire passer quoi que ce soit en fraude, parce qu'il n'y a pas de shell dans le chemin. Toute la technique de GuardFall a besoin d'un bash entre le contrôle et l'action ; sur le chemin des Guardrails, il n'y en a pas.

Ce autour de quoi cela trace une ligne, pour être clairs

La commande s'exécute quand même

Bromure est de l'isolation, pas de l'interception. Une charge GuardFall qui glisse le garde propre à votre agent s'exécutera dans la VM. Ce que Bromure change, c'est l'impact : un home jetable, des identifiants leurres, un chemin de sortie journalisé. Si vous avez besoin que la commande elle-même soit refusée, c'est le travail du garde de votre agent — et GuardFall est la raison pour laquelle vous ne devriez pas vous y fier.

La destruction locale, c'est de la jetabilité, pas un blocage

Un find /x -delete contre des fichiers dans la VM n'est pas un appel d'API fournisseur, donc les Guardrails ne le contrôlent pas. La réponse à la destruction dans la VM, c'est que la VM est jetable et que le vrai travail vit dans un dépôt monté et sur l'hôte, pas que le delete a été stoppé. Réinitialiser à la base, pas revenir en arrière.

La substitution couvre les identifiants que vous configurez

L'échange brm_… protège les secrets que vous mettez dans un profil — clés de modèle, jetons cloud et git, points de terminaison de bases de données managées. Un mot de passe que vous collez à la main dans un fichier de la boîte, ou un jeton qu'un script écrit sur disque en cours de session, n'est qu'un fichier. Gardez les secrets dans le courtier, pas dans l'espace de travail.

La sortie est journalisée, pas interdite par défaut

La balise d'exfiltration apparaît dans le Journal de sécurité ; c'est de l'attribution, pas de la prévention. Rien de réel ne part parce que les identifiants sont des leurres, mais si vous avez besoin que la VM soit empêchée de parler à des hôtes arbitraires, c'est une politique réseau que vous devez encore définir.

La part qui se généralise

GuardFall est un énoncé net d'une règle que toute la catégorie n'en finit pas de réapprendre : vous ne pouvez pas rendre un agent sûr en demandant à un filtre de prédire ce que sa commande va faire. Le modèle écrit une chaîne, et quelque part en dessous un shell, un client d'API, un gestionnaire de paquets ou un navigateur réinterprète cette chaîne avec ses propres règles. Toute défense qui inspecte la sortie de l'agent et espère qu'elle correspond au comportement final parie contre cet écart, et l'écart gagne toujours — il a gagné ici dans dix des onze agents qui étaient, selon les mots d'Adversa, pleinement activés et correctement configurés.

Bromure Agentic Coding ne prend pas ce pari. Il suppose que la commande va s'exécuter, que le garde va parfois manquer, et que l'agent que vous avez invité peut être retourné contre vous par un README que vous n'avez jamais écrit. Alors il rend l'endroit où la commande s'exécute sans intérêt à attaquer : de vraies clés sur l'hôte, des leurres dans la boîte, une VM jetable, un blocage ferme réservé au fil où aucun shell ne peut le réécrire, et un journal de tout ce qui a tenté de partir. Vous pouvez faire tourner n'importe lequel des dix agents que GuardFall a cassés — l'issue est la même boîte jetable dans tous les cas. C'est libre et open source.