Using Python’s built-in subprocess module is extremely powerful for executing external commands and managing child processes. However, it also introduces significant security risks if misused.
Python’s subprocess module provides no mechanism to attach to the standard I/O streams of an already-running external process.
Security Concerns¶
Legacy subprocess functions carry a high severity rating. You should migrate away from the older APIs to the modern subprocess.run() (or subprocess.check_* variants where appropriate).
Functions that should be avoided or reviewed carefully:
subprocess.call()subprocess.Popen()(when used directly)subprocess.check_call()subprocess.check_output()subprocess.getoutput()subprocess.getstatusoutput()
Rationale¶
The subprocess module allows Python code to execute external system commands. This capability is powerful but inherently dangerous, as it creates a direct bridge between application logic (often fed by untrusted input) and the operating system shell.
Common security vulnerabilities arise when:
User-controlled input is interpolated into command strings
The shell is invoked (
shell=True)Arguments are passed as strings instead of lists
Output or exit codes are trusted without proper validation
Errors are silently ignored or mishandled
If an attacker can influence any part of the constructed command, they may achieve command injection, privilege escalation, data exfiltration, or remote code execution (RCE).
All listed APIs are safe by default only when used correctly — but they are very easy to misuse.
Preventive measures¶
These guidelines apply to all subprocess APIs:
Avoid
shell=Truewhenever possible
This is the single biggest risk factor. Shell metacharacters (;,&&,|,$(),`, etc.) become exploitable when the shell is involved.Always pass arguments as lists, never as strings
# Good subprocess.run(["ls", "-l", "/tmp"]) # Bad subprocess.run("ls -l /tmp", shell=True)Never concatenate or interpolate user input into command strings
This applies especially to paths, filenames, flags, or filters.Validate and sanitise all inputs
Prefer allowlists over blocklists. Consider using libraries likeshlex.quote()when string construction is unavoidable.Explicitly check return codes and handle errors
Silent failures can mask partial or malicious command execution.Run subprocesses with the least privileges possible
Never execute subprocesses as root (or other high-privilege users) unless absolutely necessary.Prefer high-level Python APIs
Many common shell operations have safe, pure-Python equivalents inos,pathlib,shutil, etc.
Example¶
Bad example (subprocess.call)¶
Highly dangerous example
import subprocess
subprocess.call("rm -rf " + user_input, shell=True)Attack scenario:
user_input = "/tmp/data; rm -rf /"This would delete the entire filesystem (if permissions allow).
A bit better example¶
import subprocess
from pathlib import Path
def safe_remove(path: str) -> None:
"""Safely remove a file or directory after validation."""
target = Path(path).resolve()
# Strict validation (example)
if not target.is_relative_to("/safe/directory"):
raise ValueError("Path outside allowed directory")
subprocess.run(["rm", "-rf", str(target)], check=True)Discussion¶
Executing shell commands or calling external applications from Python is a powerful capability, but it is inherently dangerous.
Wherever possible, you should use a native Python API instead of invoking shell commands. In the vast majority of cases, this is not only feasible but also significantly more secure.
Popen is the low-level foundation and used for some advanced cases, but use run() for most cases if you need this kind of functionality.
Using Popen itself is not directly a vulnerability, but too often crucial checks are missing so within code Popen is often a weakness that should be reviewed in depth.
