Interactive Playground
Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.
pathfinder scan --ruleset python/PYTHON-FLASK-SEC-002 --project .About This Rule
Understanding the vulnerability and how it is detected
This rule detects OS command injection in Flask applications where user-controlled input from HTTP request parameters reaches subprocess module functions: subprocess.call(), subprocess.check_call(), subprocess.check_output(), subprocess.run(), subprocess.Popen(), subprocess.getoutput(), and subprocess.getstatusoutput().
The subprocess module is safer than os.system() when used with list arguments, but becomes equally dangerous when user input is passed as part of a command string, especially when shell=True is set. subprocess.getoutput() and subprocess.getstatusoutput() always invoke a shell and are functionally equivalent to os.popen() for injection purposes.
The rule traces tainted data from Flask request sources through variable assignments, helper functions, and module boundaries to the command argument at position 0. It distinguishes between subprocess.run(['cmd', user_input], ...) -- safe, the user input is a separate argument to the program -- and subprocess.run(f'cmd {user_input}', shell=True) -- dangerous, the shell interprets the whole string. The .tracks(0) parameter targets the command argument specifically, not flags or environment dictionaries passed in other positions.
Security Implications
Potential attack scenarios if this vulnerability is exploited
Shell Injection via String Interpolation
When subprocess.run() receives a formatted string and shell=True, the shell parses the entire string. A user-supplied value of "host; cat /etc/passwd" causes two commands to run. The Flask application returns the first command's output, while the attacker's injected command executes silently in the same process context.
Unrestricted Process Spawning
subprocess.Popen() gives the attacker a handle to a spawned process. With stdin/stdout/stderr piped, an attacker can establish a bidirectional channel to an injected process -- effectively an interactive shell -- through the Flask endpoint's request/response cycle.
Environment Variable Leakage
subprocess.check_output() captures stdout, which the Flask route often returns directly. Injected commands like "env" or "printenv AWS_SECRET_ACCESS_KEY" cause the response body to contain cloud credentials, database passwords, and other secrets stored in the process environment.
Chained Exploitation with Internal Services
In Kubernetes or Docker environments, the Flask container has network access to internal services not exposed to the internet. An injected curl or wget command reaches internal APIs, metadata endpoints, and databases that the public internet cannot touch.
How to Fix
Recommended remediation steps
- 1Pass commands as a list to subprocess functions -- ['program', 'arg1', 'arg2'] -- and never set shell=True when user input is involved. List arguments bypass the shell entirely.
- 2Avoid subprocess.getoutput() and subprocess.getstatusoutput() for user-influenced commands -- they always invoke a shell with no way to opt out.
- 3When shell=True is truly required (e.g., pipeline with pipes), wrap every user-supplied token with shlex.quote() before interpolation into the command string.
- 4Validate input against a strict allowlist before it reaches any subprocess call -- reject requests that contain characters outside [a-zA-Z0-9._-] for filenames and identifiers.
- 5Set timeout and resource limits on subprocess calls to prevent DoS through long-running injected commands consuming all Flask worker threads.
Detection Scope
How Code Pathfinder analyzes your code for this vulnerability
Scope: global (cross-file taint tracking across the entire project). Sources: Flask HTTP input methods -- request.args.get(), request.form.get(), request.values.get(), request.get_json(), request.cookies.get(), request.headers.get() -- all of which deliver attacker-controlled strings. Sinks: subprocess.call(), subprocess.check_call(), subprocess.check_output(), subprocess.run(), subprocess.Popen(), subprocess.getoutput(), subprocess.getstatusoutput(). The .tracks(0) setting targets argument position 0, the command string or command list. This is where injection occurs. The env dictionary and other keyword arguments at different positions are not tracked. Sanitizers: shlex.quote() and shlex.split() are recognized as sanitizing transformations. A tainted value that passes through shlex.quote() before reaching the sink is treated as safe. Note that simply splitting input with shlex.split() and passing the result as a list to subprocess removes the shell-string injection risk but does not prevent argument injection to the subprocess itself. The rule follows data through assignments, return values, and cross-file function calls, so injection chains that pass through utility modules are detected.
Compliance & Standards
Industry frameworks and regulations that require detection of this vulnerability
References
External resources and documentation
Similar Rules
Explore related security rules for Python
Django SQL Injection via RawSQL Expression
User input flows to RawSQL() expression without parameterization, enabling SQL injection through Django's annotation system.
Lambda Command Injection via subprocess
Lambda event data flows to subprocess with shell=True or as a string command, enabling OS command injection in the Lambda execution environment.
Dangerous os.exec*() Call
os.exec*() replaces the current process image with a new program, enabling arbitrary program execution when arguments are untrusted.
Frequently Asked Questions
Common questions about Flask Command Injection via subprocess
New feature
Get these findings posted directly on your GitHub pull requests
The Flask Command Injection via subprocess rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.