# Shell-Interpolation: Verhindern, dass Agenteneingaben zu Befehlen werden

Ein Agent braucht kein Shell-Tool, um eine Shell-Ausführung auszulösen. Geben Sie ihm einen Pfad, einen Branch, einen Image-Tag, einen Hostnamen oder einen Ressourcennamen. Wenn dieser Wert später in eine Befehlszeichenkette eingesetzt wird, kann unvertrauenswürdiger Text das ausgeführte Programm verändern.

Die Lösung ist keine cleverere Quoting-Funktion. Hören Sie auf, den Befehlsaufbau als Zeichenkettenformatierung zu behandeln. Eine Aktion sollte eine feste Operation benennen, typisierte Felder mit engen Regeln akzeptieren, ein Programm mit einem echten Argumentvektor aufrufen und Berechtigungen von der Analyse der Eingaben trennen. Ich habe genug Berichte über Vorfälle gelesen, um das klar zu sagen: Ein Tool, das einen frei formulierten Befehl akzeptiert, weil es «nur für unseren Agenten» gedacht ist, wird früher oder später auch die Anweisungen anderer akzeptieren.

Agenten machen diesen Fehler leichter erreichbar. Sie verarbeiten Texte aus Issues, Repository-Inhalte, Tool-Ausgaben, Webseiten und Chatnachrichten. Jede dieser Quellen kann das nächste Argument beeinflussen. Ob der Agent Schaden beabsichtigt hat, ist dabei nebensächlich. Jede Grenze zu einem Betriebssystem oder Remote-Host muss davon ausgehen, dass ein Angreifer die Eingabe beeinflussen kann.

## Shell-Interpolation verwandelt Daten in Syntax

Shell-Interpolation entsteht, wenn Code Text für einen Befehlsinterpreter erstellt, statt einzelne Argumente an eine Prozess-API zu übergeben. Sobald eine Shell diesen Text erhält, wendet sie ihre eigenen Sprachregeln an. Trennzeichen können einen weiteren Befehl starten, Befehlssubstitutionen einen verschachtelten Befehl ausführen, Umleitungen Dateien verändern und Expansionen aus einem scheinbaren Wert mehrere Wörter machen.

Stellen Sie sich vor, ein Agent soll vor einem Release einen Branch prüfen. Dieser Handler wirkt harmlos:

```python
branch = request["branch"]
command = f"git show {branch}"
subprocess.run(command, shell=True, check=True)
```

Wenn `branch` den Wert `release; id` enthält, sieht die Shell zwei Befehle. Der erste Teil fordert Git auf, eine Revision anzuzeigen. Der zweite Teil führt `id` aus. Derselbe Fehler tritt in JavaScript mit `exec`, in Ruby mit einer an `system` übergebenen Zeichenkette, in Go mit `sh -c` und in CI-Skripten auf, die Agentenausgaben direkt in eine Shell-Variable schreiben, ohne ihre spätere Verwendung zu kontrollieren.

Das gefährliche Zeichen muss nicht im Prompt des Agenten stehen. Es kann in einem Pull-Request-Titel, einer Fehlermeldung, einem Paketmanifest oder einem Dateinamen aus einem Repository-Scan stecken. Der Agent kann diesen Inhalt in einem späteren Tool-Aufruf zitieren, weil seine Aufgabe verlangt, ihn zu prüfen oder zu reparieren. Eine Prüfung, die nur fragt, ob das Modell einer Anweisung folgt, übersieht die Stelle, an der Text zu ausführbarer Syntax wird.

Die POSIX Shell Command Language beschreibt eine Abfolge aus Expansionen, in bestimmten Fällen Feldaufteilung, Pfadnamen-Expansion und Entfernen von Anführungszeichen. Diese Reihenfolge ist wichtig. Entwickler sagen oft: «Ich habe es in Anführungszeichen gesetzt», als würden Anführungszeichen eine allgemein sichere Zeichenkette erzeugen. Das tun sie nicht. Sie begrenzen bestimmte Parsing-Regeln in einer bestimmten Shell und an einer bestimmten Stelle der Expansionsfolge.

Eine Shell kann Eingaben außerdem auf mehreren Wegen erhalten. Ein Wert kann den Befehlstext selbst, eine Umgebungszuweisung, ein Umleitungsziel, eine Befehlssubstitution, eine Shell-Startdatei oder Code beeinflussen, der später von einem anderen Interpreter ausgewertet wird. Escaping für die erste Shell schützt nicht automatisch vor dem nächsten Parser.

Halten Sie diese Unterscheidung klar:

- **Command Injection** liegt vor, wenn eine Eingabe die von einer Befehlssprache interpretierte Syntax verändert.
- **Argument Injection** liegt vor, wenn die Eingabe zwar ein Argument bleibt, aber verändert, wie das aufgerufene Programm dieses Argument interpretiert, etwa als Option oder Ausdruck.
- **Ein Berechtigungsfehler** liegt vor, wenn ein gültiges Argument eine Aktion außerhalb des erlaubten Bereichs anfordert.

Ein Argumentvektor verhindert die erste Kategorie. Die zweite und dritte löst er nicht. Ein Pfad, der aus einem Arbeitsbereich herausführt, eine Git-Revision, die ein unerwünschtes Objekt auswählt, und eine URL, die einen internen Dienst erreicht, können jeweils ein vollständig getrenntes Argument sein.

## Auch eine quotierte Befehlszeichenkette hat zu viele Parser

Eingaben vor der Verkettung in einen Shell-Befehl zu quoten, ist beliebt, weil es wie eine kleine Reparatur wirkt. Es bleibt jedoch fragil, weil ein Befehl oft mehr als eine Grammatik durchläuft.

Angenommen, ein lokales Programm erstellt einen SSH-Aufruf und das Remote-System führt eine Shell aus. Ein Entwickler quotet den Branch für die lokale Shell, ein zweites Mal für SSH und ein drittes Mal für die Remote-Shell. Jede Schicht hat andere Annahmen zu Leerzeichen, Anführungszeichen, Backslashes, Befehlssubstitution und Encoding. Eine spätere Änderung kann eine Quotierungsschicht entfernen oder die bereits quotierte Zeichenkette in einen neuen Kontext setzen. Der Code sieht weiterhin sorgfältig aus, obwohl der Schutz nicht mehr greift.

Besonders problematisch ist dieses Muster:

```javascript
const command = `git checkout '${branch}'`;
execFile("ssh", [host, command], callback);
```

`execFile` schützt den lokalen Aufruf von `ssh`, was gut ist. Es macht `command` auf der Remote-Seite nicht sicher. SSH übergibt der Remote-Seite normalerweise eine Befehlszeichenkette. Das Remote-Konto reicht diese Zeichenkette meist an eine Shell weiter. Der Branch gelangt daher einen Hop später in die Shell-Syntax.

Einfache Anführungszeichen sind keine allgemeine Lösung. Eine Eingabe mit einem einfachen Anführungszeichen kann den vorgesehenen quotierten Bereich beenden. Eine für POSIX-Shells geschriebene Quotierungsroutine ist für PowerShell falsch. Eine Routine, die für ein Shell-Wort korrekt ist, ist falsch, wenn der Wert in einer Umleitung, einem arithmetischen Ausdruck oder einem Sprachinterpreter landet. Eine heute korrekte Routine bleibt außerdem eine Wartungslast, wenn jemand `git show` in `git log --format=...` ändert und die Vorlage umstellt.

Verwechseln Sie das Serialisieren einer Argumentliste für Logs nicht mit ihrer Rekonstruktion zur Ausführung. Eine Logzeile wie `git show release-42` ist für Menschen nützlich, beweist aber nicht, wo die ursprünglichen Argumentgrenzen lagen. Sie ist keine sichere ausführbare Darstellung. Speichern Sie Argumente als strukturiertes Array und erzeugen Sie daraus nur für die Anzeige eine Darstellung mit klaren Escaping-Regeln.

Umgebungsvariablen bergen eine weitere Falle. Das ist sicherer als Interpolation:

```sh
BRANCH="$branch" git show "$BRANCH"
```

Es bleibt aber nur dann sicher, wenn das Shell-Skript beide quotierten Expansionen kontrolliert und den Wert später nie auswertet. Verwendet ein nachgelagertes Skript `eval`, erstellt es eine weitere Befehlszeichenkette oder übergibt den Wert an eine Template-Engine mit Ausführungsfunktionen, war die ursprüngliche Maßnahme kaum hilfreich. Eine bessere Grenze vermeidet es, den Agentenwert überhaupt an eine Shell zu übergeben.

## Feste ausführbare Dateien und Argument-Arrays entfernen die Shell-Grammatik

Rufen Sie lokale Aktionen mit einer festen ausführbaren Datei und einem Argument-Array auf und lassen Sie die Shell-Ausführung deaktiviert. Das Betriebssystem übergibt dem Kindprozess dann die einzelnen von Ihnen gelieferten Zeichenketten. Es interpretiert Semikolons, Dollarzeichen, Leerzeichen, Klammern oder Glob-Zeichen nicht als Shell-Sprache.

Die Python-Dokumentation zu `subprocess` empfiehlt eine Sequenz von Argumenten und weist darauf hin, dass `shell=False` der Standard ist. Verwenden Sie diesen Standard bewusst, statt sich zufällig darauf zu verlassen:

```python
import subprocess

result = subprocess.run(
    ["git", "status", "--porcelain=v1"],
    cwd=repo_dir,
    text=True,
    capture_output=True,
    check=True,
)
print(result.stdout)
```

Diese Aktion enthält weder eine vom Agenten kontrollierte ausführbare Datei noch eine vom Agenten kontrollierte Befehlsschablone. Sie führt eine bekannte Operation in einem freigegebenen Verzeichnis aus. Die Ausgabe ist einfacher Text, mit einem Statusdatensatz pro Zeile. Sie können die Schnittstelle weiter verbessern, indem Sie diese Ausgabe parsen und strukturierte Datensätze zurückgeben, statt Rohtext in eine weitere Agentenentscheidung einfließen zu lassen.

Wenn eine Operation Eingaben benötigt, halten Sie ausführbare Datei und Operation fest und hängen Sie validierte Werte als separate Einträge an:

```python
subprocess.run(
    ["tool", "fetch-resource", resource_id],
    cwd=workspace,
    check=True,
    shell=False,
)
```

Verwenden Sie in Node.js bevorzugt `spawn` oder `execFile` mit einem Argument-Array. Aktivieren Sie die Option `shell` nicht, nur damit ein zusammengesetzter Befehl bequemer wird. Verwenden Sie in Go `exec.Command` und übergeben Sie jedes Argument einzeln. In Rust nutzen Sie `Command` und wiederholte Aufrufe von `arg`. Die APIs unterscheiden sich, die Invariante bleibt gleich: Keine Eingabe gelangt in eine Befehlssprache.

Hier lohnt sich ein Einwand gegen eine häufige Empfehlung: «Verwenden Sie ein Shell-Skript als sicheren Wrapper.» Ein festes Shell-Skript kann eine akzeptable Kompatibilitätsgrenze sein, ist aber nicht allein deshalb sicherer, weil es ein Skript ist. Es wird erst sicher, wenn es Werte über feste Positionsparameter oder die Standardeingabe erhält, jede Expansion im Skript quotiert, `eval` vermeidet und keine zweite Befehlszeichenkette erstellt. Ein kleines Programm mit einer Prozess-API lässt sich meist leichter prüfen.

Es gibt legitime Shell-Funktionen: Pipelines, bedingte Abläufe, Umleitungen und Shell-Builtins. Legen Sie diese Funktionen in ein geprüftes statisches Skript, wenn Sie sie nicht entfernen können. Lassen Sie den Agenten keine Pipeline zusammensetzen. Geben Sie ihm eine Aktion namens `collect_build_logs`, während der Wrapper seine feste Pipeline mit festen Dateipfaden und begrenzten Parametern ausführt.

Auch die ausführbare Datei gehört zur Richtlinie. Wählen Sie sie niemals aus Agenteneingaben, einer beschreibbaren Konfigurationsdatei oder über einen von einem Angreifer kontrollierten `PATH`. Verwenden Sie, wo es Ihre Umgebung erfordert, einen absoluten, vom Administrator kontrollierten Pfad. Setzen Sie ein bekanntes Arbeitsverzeichnis und eine reduzierte Umgebung, statt beliebige Variablen aus dem Agentenprozess zu übernehmen.

## Validierung legt fest, was die Aktion bedeuten darf

Argument-Arrays schützen die Parser-Grenze. Validierung schützt die Aktionsgrenze. Wenn eine Aktion `read_file` heißt, muss der Dienst entscheiden, welche Dateien diese Aktion lesen darf. Eine Prozess-API kann diese Entscheidung nicht für Sie treffen.

Beginnen Sie mit einem engen Schema. Ein Deployment-Name kann aus Kleinbuchstaben, Ziffern und einzelnen internen Trennzeichen bestehen und eine Längenbegrenzung haben. Ein Branch-Selektor kann ein Branch-Name aus einer ausgewählten Teilmenge der Git-Regeln sein. Eine Ressourcenkennung kann eine UUID oder eine Inventarnummer sein. Ein Serverziel kann eine ID sein, die Ihr Dienst einem gespeicherten Host zuordnet, statt ein beliebiger Hostname.

Vermeiden Sie allgemeine Blocklisten wie «Semikolon und Et-Zeichen ablehnen». Sie richten sich nach einem Parser und lassen semantischen Missbrauch unangetastet. Außerdem wachsen sie so lange, bis normale Benutzer nicht mehr vorhersehen können, was funktioniert. Eine Allowlist-Grammatik gibt dem Aufrufer einen Vertrag und dem Prüfer eine begrenzte Menge von Eingaben.

Für ein Branch-Feld in einem Release-Workflow kann eine bewusst begrenzte Grammatik besser sein, als jede von Git erlaubte Referenz zu akzeptieren:

```python
import re

BRANCH = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/]{0,127}")

def release_ref(value: str) -> str:
    if not isinstance(value, str):
        raise ValueError("branch must be text")
    if not BRANCH.fullmatch(value):
        raise ValueError("branch has unsupported characters")
    if "/./" in value or "//" in value or value.endswith("/"):
        raise ValueError("branch has an unsupported path form")
    return "refs/heads/" + value
```

Dieser Code behauptet nicht, Git's vollständige Ref-Grammatik umzusetzen. Er definiert absichtlich eine kleinere Grammatik für eine Release-Aktion. Das ist oft die richtige technische Entscheidung. Wenn Benutzer Leerzeichen oder eine andere ungewöhnliche Form brauchen, nehmen Sie sie auf, weil der Workflow sie benötigt, testen Sie sie und dokumentieren Sie sie. Übernehmen Sie nicht jeden Sonderfall eines allgemeinen Versionskontrollsystems, wenn Ihre Aktion nur Release-Branches ausrollt.

Der zurückgegebene Wert hat ein festes Namespace-Präfix. Das ist wichtig. Ein beliebiger Revisionsausdruck kann auf Tags, Commit-Abstammungen, Reflogs oder Syntax verweisen, die ein Git-Befehl speziell interpretiert. Eine Aktion für Release-Branches sollte nicht unbemerkt zu «zeige jedes Git-Objekt, das der Agent beschreiben kann» werden. Verwenden Sie eine Bibliothek oder Gits eigene Branch-Validierung, wenn Sie vollständige Git-Kompatibilität brauchen, und ordnen Sie das akzeptierte Ergebnis trotzdem dem erlaubten Namespace zu.

Ressourcennamen brauchen dieselbe Disziplin. Wenn ein Agent ein Cloud-Objekt anfordert, speichern Sie erlaubtes Konto, Region, Bucket oder Projekt außerhalb des frei formulierten Feldes. Lassen Sie ihn einen Objektnamen liefern, der der von Ihnen unterstützten Grammatik entspricht. Akzeptieren Sie keine vollständige URL und nennen Sie sie dann einen Ressourcennamen. Eine vollständige URL wählt Protokoll, Host, Port, Pfad und manchmal Zugangsdaten. Das ist eine Netzwerkberechtigungsentscheidung, die als Zeichenkette getarnt ist.

Längenbegrenzungen gehören zur Validierung. Sie begrenzen das Logwachstum, verhindern unbeabsichtigte Kommandozeilenlimits und erschweren Denial of Service. Auch Regeln für die Zeichenkodierung gehören dazu. Lehnen Sie Steuerzeichen in Textfeldern ab, sofern die Aktion sie nicht wirklich benötigt. NUL müssen Sie bei Prozessargumenten immer ablehnen, weil Betriebssystemschnittstellen sie nicht als Teil eines Arguments übertragen können.

## Für Pfade braucht man Begrenzungsprüfungen, keine Zeichenfilter

Ein Pfad kann für die Shell sicher und für das Dateisystem gefährlich sein. `../../secrets.env` enthält keines der Metazeichen, auf die Quotierungsfunktionen achten. Trotzdem kann der Pfad eine Dateioperation aus dem Arbeitsbereich herausführen.

Eine sichere Pfadrichtlinie beginnt mit einem Stammverzeichnis für die Aktion. Lösen Sie den angeforderten relativen Pfad gegen dieses Stammverzeichnis auf und verlangen Sie anschließend, dass das aufgelöste Ziel innerhalb des ebenfalls aufgelösten Stamms bleibt. Akzeptieren Sie keinen absoluten Pfad, wenn die Aktion nur Arbeitsbereichsdateien benötigt. Normalisieren Sie nicht einfach eine Zeichenkette und nehmen Sie an, ihre Schreibweise zeige, wohin das Dateisystem gehen wird.

Eine konzeptionelle Prüfung sieht so aus:

```python
from pathlib import Path

workspace = Path("/srv/agent-workspaces/repo-a").resolve()

def permitted_file(relative_name: str) -> Path:
    if not isinstance(relative_name, str) or relative_name.startswith("/"):
        raise ValueError("file must be a relative path")
    candidate = (workspace / relative_name).resolve(strict=True)
    if candidate == workspace or workspace not in candidate.parents:
        raise ValueError("file is outside the workspace")
    if not candidate.is_file():
        raise ValueError("requested target is not a regular file")
    return candidate
```

Der Aufruf `resolve` erkennt gewöhnliche Traversal-Versuche und folgt vorhandenen Symlinks. Das ist besser, als in der Rohzeichenkette nach `..` zu suchen. Für einen sensiblen Schreibvorgang ist es trotzdem nicht die vollständige Lösung. Ein Angreifer, der den Verzeichnisbaum verändern kann, könnte nach der Prüfung und vor dem späteren Öffnen eine Pfadkomponente durch einen Symlink ersetzen. Diese Lücke zwischen Prüfung und Nutzung ist eine Time-of-Check-to-Time-of-Use-Race.

Für Leseoperationen in einem kontrollierten Arbeitsbereich kann eine aufgelöste Begrenzungsprüfung zu Ihrem Risiko passen. Für Schreibvorgänge, Löschungen, Berechtigungsänderungen oder Archive, die Angreifer beeinflussen können, sollten Sie Dateisystemoperationen verwenden, die Verzeichnis-Deskriptoren halten und Symlink-Traversal verweigern, sofern das Betriebssystem diese Kontrollen bietet. Trennen Sie beschreibbare Agenten-Arbeitsbereiche von dem Code, der erlaubte Stammverzeichnisse festlegt. Wenn Ihr Bedrohungsmodell einen lokalen Angreifer umfasst, der das Dateisystem manipulieren kann, reicht eine Pfadbibliothek auf hoher Ebene nicht aus.

Entscheiden Sie außerdem, ob versteckte Dateien, Repository-Metadaten, erzeugte Abhängigkeitsverzeichnisse und symbolische Links zum erlaubten Bereich gehören. Eine unklare Regel wie «alles innerhalb des Repositories» legt oft Credential-Dateien, Build-Caches oder Konfigurationen offen, die der Workflow nie benötigt hat. Leseberechtigungen sollten dem Zweck der Aktion folgen, nicht dem Komfort eines rekursiven Globs.

Das Entpacken von Archiven verdient eine eigene Warnung. Validieren Sie jedes Archivmitglied, nachdem Sie es mit dem Zielstamm verbunden haben, und behandeln Sie Links innerhalb eines Archivs als potenziell feindlich. Path Traversal ist nicht auf Anfragen beschränkt, die der Agent direkt stellt. Ein Tool, das ein vom Agenten ausgewähltes Artefakt entpackt, kann dieselbe Flucht in seinem Auftrag ausführen.

## Optionen und Ausdrücke bleiben auch ohne Shell gefährlich

Ein Argumentvektor hält einen Wert von der Shell-Syntax getrennt, aber das aufgerufene Programm parst diesen Wert weiterhin. Programme behandeln Argumente, die mit einem Bindestrich beginnen, häufig als Optionen. Manche akzeptieren Ausdrücke, Konfigurationsverweise, Syntax zum Einbinden von Dateien, Plugins oder Command Hooks. Ein untrusted Wert als ein einzelnes Argument macht diese Bedeutungen nicht harmlos.

Nehmen wir einen Wrapper, der ein Suchprogramm mit einem vom Benutzer gelieferten Muster aufruft. Der Wrapper kann korrekt `["search", pattern, directory]` übergeben. Wenn das Programm das Muster als regulären Ausdruck behandelt, kann ein pathologischer Ausdruck enorme CPU-Zeit verbrauchen. Unterstützt es eine Option zum Lesen einer Konfigurationsdatei und landet das Muster an der falschen Position, kann der Aufrufer das Verhalten des Programms verändern. Unterstützt die Sprache des Programms Codeausführung, haben Sie dem Agenten einen anderen Interpreter übergeben.

Setzen Sie feste Optionen vor die Daten und verwenden Sie den üblichen End-of-Options-Marker des Programms, sofern es ihn unterstützt. In Fließtext besteht dieser Marker aus zwei Bindestrichen. Er teilt vielen Kommandozeilenparsern mit, dass spätere Werte Operanden und keine Flags sind. Verlassen Sie sich bei der Validierung nicht darauf. Manche Tools unterstützen den Marker nicht, und ein Operand kann semantisch weiterhin gefährlich sein.

Git hat eine große Befehlsoberfläche. Deshalb brauchen Agenten enge Git-Aktionen statt eines allgemeinen «Git ausführen»-Notausgangs. Eine Aktion `show_release_branch` kann einen eingeschränkten Branchnamen akzeptieren, ihn in `refs/heads/` plus diesen Namen umwandeln und einen festen Git-Unterbefehl aufrufen. Eine Aktion `checkout_anything`, die beliebige Revisionssyntax akzeptiert, hat eine breitere Bedeutung und braucht eine umfassendere Berechtigungsentscheidung. Das sind unterschiedliche Produkte, auch wenn beide zufällig einen Git-Prozess starten.

Dieselbe Regel gilt für Paketmanager, Datenbankclients, Medientools, Archivprogramme und Infrastruktur-Befehle. Fragen Sie, welche Grammatik das Zielprogramm parst, nachdem das Betriebssystem das Argument übergeben hat. Eine Befehlszeile ist oft nur der erste Parser in einer Kette.

Bei regulären Ausdrücken ist besondere Vorsicht nötig. Ein Muster ist kein Shell-Code, aber es ist ausführbare Arbeit innerhalb einer Regex-Engine. Wenn der Agent Text durchsuchen muss, wählen Sie nach Möglichkeit eine Regex-Engine mit vorhersehbarer Laufzeit, begrenzen Sie Muster und Eingaben und machen Sie die Literalsuche zur Standardaktion. Verwandeln Sie nicht jede Suchanfrage in eine uneingeschränkte Ausdruckssprache, nur weil ein Entwickler einen flexiblen Endpunkt möchte.

## SSH macht aus einem lokalen Befehl ein Remote-Parsing-Problem

SSH schafft eine zweite Ausführungsgrenze. Sie können den lokalen SSH-Client mit einem perfekten Argument-Array aufrufen und trotzdem eine unsichere Befehlszeichenkette an den Remote-Host senden. Viele SSH-Implementierungen führen den angeforderten Remote-Befehl über die Shell des Remote-Kontos oder einen vergleichbaren Befehlsparser aus.

Erstellen Sie keine Remote-Befehle aus Agentenfeldern. Das ist selbst dann falsch, wenn der lokale Aufruf ein Array verwendet:

```python
remote = "deploy " + environment + " " + branch
subprocess.run(["ssh", host, remote], check=True)
```

Eine sichere Anordnung sendet einen konstanten Remote-Befehl und überträgt Daten über einen strukturierten Kanal. Das Remote-Konto kann beispielsweise ein geprüftes Programm an einem festen Pfad bereitstellen. Die lokale Seite startet dieses feste Programm, sendet eine JSON-Anfrage über die Standardeingabe und das Remote-Programm validiert `environment` und `branch`, bevor es eine lokale Operation ausführt.

```python
import json
import subprocess

request = {"environment": "staging", "branch": "release/42"}
subprocess.run(
    ["ssh", "deploy.internal", "/usr/local/libexec/receive-deploy-request"],
    input=json.dumps(request) + "\n",
    text=True,
    check=True,
)
```

Der Remote-Befehl in diesem Beispiel ist konstant. Das JSON ist eine Eingabe über die Standardeingabe und kein in den Remote-Befehl eingebetteter Text. Der Remote-Empfänger muss weiterhin unbekannte Felder ablehnen, Längenbegrenzungen durchsetzen, jeden Wert validieren und für jeden Kindprozess ein Argument-Array verwenden. Strukturierter Transport beseitigt das Quotierungs-Labyrinth. Er schafft kein Vertrauen.

Der Hostname sollte normalerweise über eine undurchsichtige Umgebungs-ID aus einer gespeicherten Konfiguration ausgewählt werden. Wenn der Agent `host` liefern darf, kann er entscheiden, wohin Zugangsdaten gehen und welche Netzwerkgrenze die Aktion überschreitet. Wenn ein Workflow wirklich mehrere Hosts benötigt, ordnen Sie genehmigte Namen wie `staging` und `production` bekannten Verbindungseinstellungen zu. Lassen Sie die Hostprüfung aktiviert. Ein Tool, das die Prüfung aus Bequemlichkeit bei der Einrichtung deaktiviert, hat einen wichtigen Teil der Grenze entfernt.

Senden Sie keine Zugangsdaten im Remote-Befehl, in der Kommandozeile oder in der JSON-Anfrage. Verwenden Sie die vorhandene Authentifizierung des Remote-Kontos und beschränken Sie dieses Konto auf die notwendige Aktion. Ein Deployment-Empfänger, der nur eine Anwendung ausrollen kann, ist leichter zu beurteilen als eine Agentensitzung mit weitreichenden Shell-Rechten.

## Genehmigungen können ein unsicheres Aktionsdesign nicht reparieren

Menschliche Genehmigungen und ein Audit-Trail sind nützliche Kontrollen, beantworten aber andere Fragen als die Validierung. Eine Genehmigung beantwortet, ob dieser Aufrufer jetzt eine Aktion versuchen darf. Validierung beantwortet, ob die Anfrage eine definierte, erlaubte Form hat. Logging beantwortet, was passiert ist und ob sich der Datensatz später verändert hat. Wer eine dieser Kontrollen als Ersatz für die anderen behandelt, baut schwache Tools mit ansprechenden Oberflächen.

Eine einmalige Sitzungsgenehmigung kann für ein festes Aktionsgateway geeignet sein. Sie ist keine sorgfältige Prüfung jeder interpolierten Zeichenkette, die ein Prozess während der restlichen Sitzung an eine Shell übergibt. Eine Genehmigung pro Aufruf gibt Menschen mehr Möglichkeiten, ein ungewöhnliches Ziel zu bemerken. Trotzdem sollte niemand verschachtelte Quotierungsregeln prüfen oder ein bösartiges Unicode-Homoglyph in einem dichten Dialog erkennen müssen.

Die Genehmigungskarte sollte die semantische Anfrage anzeigen: `deploy branch release/42 to staging`, nicht eine aus einer Vorlage rekonstruierte Shell-Befehlszeile. Der Empfänger sollte die angezeigte Umgebung und den Branch mit dem Anfrage-Schema vergleichen können. Wenn ein Befehl ein rohes Skriptfeld braucht, um sich zu erklären, ist die Aktion für eine verlässliche Genehmigung zu breit.

Protokollieren Sie strukturierte Fakten an der Aktionsgrenze. Bewahren Sie Identität des Aufrufers oder Agentenprozesses, genehmigten Aktionsnamen, separate Argumente oder Anfragefelder, ausgewähltes Ziel, Zeitstempel, Exit-Status und Ausgabeverweise gemäß Ihren Aufbewahrungsregeln auf. Erfassen Sie auch Validierungsfehler. Wiederholte abgelehnte Traversal-Zeichenketten oder optionsartige Werte können auf einen schlechten Prompt, einen Integrationsfehler oder aktives Ausprobieren hinweisen.

Für Teams, die Sallyport verwenden, können die sitzungsbezogene Autorisierung, die Genehmigung des Schlüssels pro Aufruf sowie die getrennten Journale Sessions und Activity die Kontrolle und Nachweise rund um Agentenaktionen bewahren. Die Befehlsschablone muss trotzdem nach den oben genannten Regeln entworfen sein.

Ein manipulationssicheres Log hilft nach einem Vorfall nur, wenn es die entscheidenden Grenzen erfasst. Eine Zeichenkette wie `ssh deploy internal deploy staging release 42` ist im Nachhinein mehrdeutig. Ein Ereignis mit separater Hostidentität, festem Empfängerpfad, JSON-Feldern, Autorisierungsergebnis und Exit-Status des Empfängers kann eine Untersuchung unterstützen.

## Testen Sie den Ablehnungspfad genauso gründlich wie den Erfolgsfall

Die meisten Tests für Command Injection beweisen nur, dass ein normaler Branch wie `release/42` funktioniert. Das ist die uninteressanteste Eingabe. Die Testsuite sollte beweisen, dass der Handler gefährliche Werte ablehnt, bevor er einen Kindprozess startet oder einen Pfad öffnet.

Erstellen Sie für jeden Feldtyp eine kleine Tabelle. Die genauen Eingaben hängen von der Grammatik ab, aber die Kategorien sollten Shell-Trennzeichen, Befehlssubstitutionen, Anführungszeichen, Leerzeichen und Zeilenumbrüche, Präfixe wie bei Optionen, Traversal-Komponenten, leere Werte, Steuerzeichen, übergroße Werte und gültige Syntax enthalten, die eine Aktion außerhalb des erlaubten Bereichs anfordert. Testen Sie bei einer Branch-Aktion außerdem einen tagähnlichen Namen und einen Revisionsausdruck, den Ihre Schnittstelle nicht unterstützt.

Verwenden Sie in Unit-Tests einen Prozess-Runner als Testdouble. Prüfen Sie die ausführbare Datei und jedes Argument als separate Werte. Prüfen Sie, dass ungültige Eingaben den Runner nie aufrufen. Ein Test, der nur eine gerenderte Befehlszeichenkette vergleicht, kann genau die Grenze übersehen, die Sie schützen wollen.

```python
class RecordingRunner:
    def __init__(self):
        self.calls = []

    def run(self, argv, **kwargs):
        self.calls.append((argv, kwargs))

def test_rejects_branch_separator():
    runner = RecordingRunner()
    try:
        release_ref("release/42; id")
    except ValueError:
        pass
    else:
        raise AssertionError("expected rejection")
    assert runner.calls == []
```

Dieser Test prüft die Richtlinienfunktion, nicht die Shell. Ergänzen Sie auch einen Integrationstest für die Prozessgrenze. Führen Sie ein harmloses Testprogramm aus, das die empfangenen Argumente mit eindeutigen Indizes und Längen ausgibt. Übergeben Sie Eingaben mit Leerzeichen, Anführungszeichen, Wildcards, Klammern und Zeilenumbrüchen. Bestätigen Sie, dass jeder Wert als ein Argument ankommt und kein zweites Programm startet.

Fuzzing ist nützlich, sobald die Aktion ein Schema besitzt. Erzeugen Sie zufälliges Unicode innerhalb und außerhalb der erlaubten Menge und erwarten Sie eines von zwei Ergebnissen: Der Validator lehnt den Wert ab oder die feste Operation erhält ihn als ein begrenztes Feld. Fuzzing darf nicht gegen ein produktives Deployment-Ziel laufen. Ziel ist, Parserannahmen in Gateway und Empfänger zu finden, bevor der Agent eine reale Umgebung mit Zugangsdaten erreicht.

Prüfen Sie zum Schluss jede Stelle, an der strukturierte Eingaben wieder in Text verwandelt werden. Dazu gehören Log-Viewer mit anklickbaren Befehlen, erzeugte Shell-Snippets, CI-Konfigurationen, Fehlerberichte, Chatbenachrichtigungen und Remote-Wrapper. Injection-Fehler kehren oft zurück, wenn eine sichere strukturierte Anfrage in einer späteren Komponente zu einer bequemen Zeichenkette wird.

Die erste Aktion, die Sie reparieren sollten, ist meist die flexibelste. Ersetzen Sie `run_command(command)` durch eine Operation mit Namen, kleinem Anfrage-Schema, fester ausführbarer Datei und einer Testsuite voller Eingaben, die niemand während einer normalen Demo eingeben würde. Diese Änderung entfernt eine ganze Klasse von Modellverhalten aus Ihrem Bedrohungsmodell und hinterlässt Regeln, die Sie tatsächlich prüfen können.
