flawopen.com/Command Injection/Python

Command Injection in Python

Critical CWE-78 Draft — pending review
ELI5

Imagine asking an assistant to print a document named 'report.pdf'. Command injection happens when someone gives the filename 'report.pdf; rm -rf /', and the assistant blindly hands that whole sentence to the computer terminal, causing it to print the report and then wipe the hard drive.

Key terms on this page
shell interpolation
When a command is passed to a shell interpreter (like /bin/sh or cmd.exe) as a single string, causing shell metacharacters (;, |, &&) to be parsed as command separators.
direct execution
Invoking a binary directly via operating system system calls (execve) where arguments are passed as discrete array elements that can never become new commands.

What's happening

In Python, command injection almost always stems from invoking os.system() or subprocess.run(..., shell=True) with string concatenation or f-strings. When shell=True is set, Python spawns an intermediate system shell (/bin/sh -c) to parse the string, enabling attackers to inject additional shell instructions.

Real-world impact

In 2021, a command injection vulnerability in Log4j (and countless network appliance admin portals including Palo Alto PAN-OS and Fortinet) allowed unauthenticated remote attackers to execute arbitrary root shell commands, leading to widespread ransomware deployments.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
# shell=True spawns /bin/sh to interpret the string
import subprocess

def ping_host(host):
    # Attacker input: "8.8.8.8; cat /etc/passwd"
    cmd = f"ping -c 1 {host}"
    return subprocess.run(cmd, shell=True, capture_output=True)
FIXED
# shell=False (default): passes arguments directly to the binary
import subprocess

def ping_host(host):
    # host is treated strictly as a single argument to ping
    cmd = ["ping", "-c", "1", host]
    return subprocess.run(cmd, shell=False, capture_output=True, check=True)

Why the fix works

When passing arguments as a list with shell=False, Python calls the OS kernel's execve directly. The host argument is passed as a discrete memory buffer to the ping binary; no shell parser is invoked, meaning semicolons, pipes, and backticks have zero syntactic meaning.

Gotchas

os.system() is always shell=True

os.system() has no array syntax. It always passes its string argument to /bin/sh -c. It should never be used with user-controlled input.

shlex.quote() has platform-specific pitfalls

shlex.quote() is POSIX-specific and fails on Windows (where cmd.exe uses different escape semantics). Avoiding shell execution entirely is vastly superior to escaping.

Common misconceptions

"Validating input with regex is enough"

Regex character blacklists almost always miss shell metacharacters like newlines ( ), IFS variable substitutions, or Unicode separators. Direct execution eliminates the need to guess metacharacters.

How to check if you're affected

grep -rn "subprocess.*shell=True" --include="*.py" . grep -rn "os\.system(" --include="*.py" .
Enable Bandit rule B602 (subprocess_popen_with_shell_equals_true) in your CI pipeline.

Prevention checklist

FAQ

When is shell=True actually required?

Only when you specifically require built-in shell features like environment variable expansion ($HOME) or pipe chaining. In those cases, use Python's built-in libraries (os.environ, subprocess.PIPE) instead of the shell.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: SQL Injection XSS Path Traversal Insecure Deserialization