flawopen.com/command-injection/Python
OS Command Injection in Python (subprocess, os.system)
os.system(), an f-string passed to subprocess with shell=True, or a user value that starts with a dash lets a request run extra commands on your server with the application's privileges (CWE-78). The safe subprocess call, why an argument list fixes the shell problem but not option injection, and the CI rules that catch both.
A self-service print shop asks for a page count, but its oldest machine reads the whole handwritten job line as a sequence of operating instructions. A semicolon can turn the requested page count into a second job. A newer machine accepts separate fields, yet the printer still treats a leading dash as a special option. Python has both boundaries: os.system() and subprocess.run(..., shell=True) invoke a shell; an argument list avoids shell parsing, but the called program still parses its own options.
Core Concepts & Subsystem Terms
os.system() / os.popen()os.system()andos.popen()pass a command string to the platform shell (for example,/bin/sh -con Unix). Shell metacharacters such as;,|,&&,$( )and backticks are syntax there. Neither API provides ashell=Falseargument-list form; use them only for a fixed command with no dynamic values.subprocess.run(..., shell=True)- Same problem as
os.system: withshell=Truethe first argument is parsed by the shell. An f-string or%in that argument puts user text where the shell reads operators. Argument list with shell=False- With
shell=False(the default) and a list such as["ping", "-c", "1", host], Python callsexecvedirectly. Each list item is one argument;;or$( )insidehostare just characters. Argument injection and Git option boundaries- Separate argv entries prevent shell parsing, but the target program still parses its own options. For an untrusted Git revision, use
git rev-parse --verify --end-of-options "$revision": this ends option parsing.--has command-specific operand or path semantics; ingit log, it separates revisions from paths. These markers are not interchangeable. Validate that the value is a revision before use. shlex.quote()shlex.quote()is intended for Unix/POSIX shells and is not a portable substitute for an argument list on Windows or other shell implementations. Avoid shell parsing where possible; withshell=False, pass a list and validate the target program's arguments separately.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-78
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Request value reaches the helper
A diagnostics page passes host to ping(), or an upload handler passes a filename to make_thumbnail(); the code calls os.system() or subprocess.run().
An f-string builds a command line
os.system() passes its string to the platform shell, and subprocess.run(..., shell=True) does the same. Shell syntax can split one request into another command; shell behavior differs by operating system.
The shell splits it into two commands
With shell=False and an argument list, ; is ordinary data, but a Git ref beginning with --output can still be parsed as an option. Python’s list prevents shell parsing; it does not validate paths or the target program’s arguments.
The attacker runs code on the server
The process can use the app account’s files, credentials, and network access. Pass a list with shell=False, validate host and filename values, keep paths within the upload directory, and put --end-of-options before Git revisions.
Source Code: Flaw vs. Secure Implementation
# tools.py: each helper below lets request text reach a command line
import os
import subprocess
# os.system passes the command string to the platform shell
def ping(host):
return os.system(f"ping -c 1 {host}")
# shell=True: the f-string is parsed by the shell
def make_thumbnail(filename):
subprocess.run(f"convert uploads/{filename} -resize 200x200 thumbs/{filename}", shell=True, check=True)
# no shell, but a ref starting with - is read as a Git option
def commit_log(ref):
return subprocess.run(["git", "log", ref], capture_output=True, text=True, check=True).stdout
# tools.py: argument lists, strict patterns and an end-of-options marker
import re
import subprocess
HOST_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9.-]{0,252}")
IMAGE_RE = re.compile(r"[A-Za-z0-9_-]{1,64}\.(?:png|jpe?g)")
# validated host passed as its own argument, no shell
def ping(host):
if not HOST_RE.fullmatch(host):
raise ValueError("invalid host name")
return subprocess.run(["ping", "-c", "1", host], check=False).returncode
# each list item is one argument; ; and $( ) are plain characters
def make_thumbnail(filename):
if not IMAGE_RE.fullmatch(filename):
raise ValueError("invalid image name")
subprocess.run(["convert", f"uploads/{filename}", "-resize", "200x200", f"thumbs/{filename}"], check=True)
# --end-of-options makes Git treat ref as a revision, never an option
def commit_log(ref):
result = subprocess.run(["git", "log", "--end-of-options", ref, "--"], capture_output=True, text=True, check=True)
return result.stdout
Engineering & System Hardening Checklist
- ✓Fail CI on Bandit B602 (
shell=True) and B605 (os.system,os.popen), or Ruff S602/S605. - ✓Call programs with
subprocess.run([...])andshell=False; never format user input into a command string. - ✓Reject option-like values by default. For an untrusted Git revision, call
git rev-parse --verify --end-of-options "$revision"; use--only with the operand or path meaning documented by that exact command. - ✓Prefer a library over a subprocess when one exists (for example Pillow instead of
convert,socketinstead ofping). - ✓Run the service as an unprivileged user with no shell access to secrets, and set
timeout=on everysubprocess.runcall. - ✓For uploaded files, generate server-side names, resolve the final path under the trusted upload directory, and verify containment;
shell=Falsedoes not prevent path traversal, symlink escapes, or unsafe file processing.
Sources
- CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- Python docs — subprocess: Security considerations
- Python docs — shlex.quote
- Bandit — B602 subprocess_popen_with_shell_equals_true
- Git — git rev-parse --end-of-options
- OWASP OS Command Injection Defense Cheat Sheet
- Python docs — os.system
- POSIX Utility Syntax Guidelines
- Git — Command-Line Interface Conventions
- Git — git log