Python subprocess shell=True Command Injection

Setting shell=True in Python's subprocess module executes commands through the system's underlying command shell rather than invoking the executable directly. When applications combine shell=True with unvalidated user input, attackers can leverage shell metacharacters to alter command logic, execute arbitrary system commands, and compromise the host environment. This article explains the technical mechanics behind this vulnerability, demonstrates how attacks occur, and outlines safe practices to prevent command injection in Python applications.

How shell=True Works

By default, functions in Python's subprocess module (such as subprocess.run(), subprocess.Popen(), and subprocess.check_output()) have shell=False. In this default mode, Python bypasses the shell and invokes the target binary directly using low-level operating system APIs (such as execve on POSIX systems or CreateProcess on Windows). Arguments are passed directly as an array of distinct strings.

When shell=True is enabled, Python launches the system shell first—typically /bin/sh -c on POSIX platforms or cmd.exe /c on Windows—and passes the entire command string to the shell process.

The Root Cause of the Vulnerability

The vulnerability arises because the operating system shell parses and interprets reserved metacharacters before executing the command. These characters include:

If an application constructs a command string by concatenating or formatting user-supplied data, the shell treats any metacharacters inside that input as control instructions rather than literal text.

Example of Vulnerable Code

Consider a script that pings an IP address provided by an external user:

import subprocess

def check_host(user_ip):
    # Vulnerable: shell=True combined with dynamic string formatting
    command = f"ping -c 1 {user_ip}"
    subprocess.run(command, shell=True)

If a benign user passes 127.0.0.1, the shell runs:

ping -c 1 127.0.0.1

If an attacker inputs 127.0.0.1; cat /etc/passwd, the shell interprets the semicolon as a command terminator and executes two separate commands sequentially:

ping -c 1 127.0.0.1
cat /etc/passwd

Because the shell processes the input, the attacker gains the ability to execute any command with the permissions of the running Python process.

How to Prevent Command Injection

Command injection caused by shell=True can be prevented through proper invocation patterns and input handling.

1. Avoid shell=True and Pass Arguments as a List

The most effective mitigation is keeping shell=False (the default) and passing the command and its arguments as a sequence of strings:

import subprocess

def check_host_safe(user_ip):
    # Safe: Direct execution without a shell
    subprocess.run(["ping", "-c", "1", user_ip], shell=False)

Without an intermediate shell process, characters like ;, |, and && are not interpreted as command separators. If an attacker inputs 127.0.0.1; cat /etc/passwd, the ping command receives the entire payload as a single, literal IP address argument, safely failing with an invalid host error.

2. Use shlex.quote() When shell=True Is Unavoidable

If an application requires built-in shell features (such as environment variable expansion or pipes), any untrusted variables must be sanitized using shlex.quote() on POSIX systems:

import shlex
import subprocess

def check_host_quoted(user_ip):
    safe_ip = shlex.quote(user_ip)
    subprocess.run(f"ping -c 1 {safe_ip}", shell=True)

shlex.quote() wraps the variable in single quotes and escapes existing single quotes, ensuring the shell treats the input as a single literal argument.

3. Enforce Strict Input Validation

Regardless of how commands are run, applications should validate input against strict allowlists (such as validating that an IP address strictly matches a standard IPv4/IPv6 pattern) before passing it to system routines.