Flask Command Injection via subprocess

CRITICAL

User input from Flask request parameters flows to subprocess functions with shell=True or as a command string. Use list arguments without shell=True.

Rule Information

Language
Python
Category
Flask
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythonflaskcommand-injectionsubprocessshell-injectioncross-fileinter-proceduraltaint-analysisCWE-78OWASP-A03
CWE References

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 .
1
2
3
4
5
6
7
8
9
10
rule.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

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

1

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.

2

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.

3

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.

4

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

OWASP Top 10
A03:2021 - Injection
CWE Top 25
CWE-78 ranked #5 in 2023 Most Dangerous Software Weaknesses
PCI DSS v4.0
Requirement 6.2.4 -- prevent injection attacks in bespoke and custom software
NIST SP 800-53
SI-10: Information Input Validation
ASVS v4.0
V5.2.2 -- verify that application protects against OS command injection

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Flask Command Injection via subprocess

Generally yes, as long as shell=False (the default). When subprocess.run() receives a list, each element becomes a separate argument to the program via exec(). The program receives them as distinct argv entries, so there is no shell to interpret metacharacters. The risk is argument injection to the child program itself (e.g., a user-supplied --config flag), which is a different vulnerability this rule does not cover.
subprocess.getoutput() is a convenience wrapper around subprocess.check_output() with shell=True hardcoded. There is no way to call it without invoking a shell, so it is always dangerous when user input reaches it. Use subprocess.run() with a list instead.
SEC-001 covers the os module (os.system, os.popen). SEC-002 covers the subprocess module. Both result in OS command execution but through different Python APIs. Running both rules together gives complete coverage of the Python shell-execution surface area in Flask applications.
If every user-supplied value is wrapped with shlex.quote() before interpolation, the rule will not flag it. However, the safer approach is to use list arguments without shell=True. Shell string construction with shlex.quote() is easy to get wrong -- one forgotten quote() call on a new code path reintroduces the vulnerability.
Only if the test code calls request.args.get() or other Flask request source methods that produce tainted data. Hardcoded string arguments to subprocess in test files are not tainted and will not trigger a finding.
Shell injection means the shell itself interprets metacharacters in user input, running additional commands. Argument injection means the user controls a flag or argument to the subprocess that changes its behavior (e.g., passing --exec to find). This rule detects shell injection via tainted argument position 0. Argument injection requires separate validation logic.
Add a step that runs: pathfinder ci --ruleset python/flask/PYTHON-FLASK-SEC-002 --format sarif --output results.sarif. Then upload the SARIF file using the github/codeql-action/upload-sarif action. Findings appear as inline annotations on pull request diffs.

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.

See how it works