# PYTHON-LANG-SEC-020: Dangerous subprocess Usage

> **Severity:** HIGH | **CWE:** CWE-78 | **OWASP:** A03:2021

- **Language:** Python
- **Category:** Python Core
- **URL:** https://codepathfinder.dev/registry/python/lang/PYTHON-LANG-SEC-020
- **Detection:** `pathfinder scan --ruleset python/PYTHON-LANG-SEC-020 --project .`

## Description

Python's subprocess module provides the recommended API for spawning child processes.
The module is significantly safer than os.system() when used correctly — passing a list
of arguments with shell=False (the default) bypasses the shell entirely and passes
arguments directly to the OS exec() syscall.

However, subprocess becomes dangerous when: (1) shell=True is used with a string command
containing user input; (2) the first argument in a list is attacker-controlled, pointing
to an arbitrary executable; or (3) arguments in the list are derived from untrusted input
without proper validation, such as injecting paths that traverse to sensitive files.

This rule audits all subprocess calls to ensure the command and arguments are safe.
subprocess.call(), subprocess.check_call(), subprocess.check_output(), subprocess.run(),
and subprocess.Popen() all require the same care.


## Vulnerable Code

```python
import subprocess
import asyncio

# SEC-020: subprocess calls
subprocess.call(["ls", "-la"])
subprocess.check_output("whoami", shell=True)
subprocess.Popen("cat /etc/passwd", shell=True)
subprocess.run("echo hello", shell=True)
```

## Secure Code

```python
import subprocess
import shlex
import re

# SECURE: Use list arguments with shell=False (default)
def run_linter(filepath: str) -> str:
    SAFE_PATH = re.compile(r'^[/a-zA-Z0-9_\-\.]+\.py$')
    if not SAFE_PATH.match(filepath):
        raise ValueError(f"Invalid file path: {filepath}")
    result = subprocess.run(
        ["/usr/bin/pylint", "--output-format=json", filepath],
        capture_output=True,
        text=True,
        timeout=60,
        check=False,
    )
    return result.stdout

# SECURE: Enumerate allowed commands with absolute paths
ALLOWED_COMMANDS = {
    "compress": ["/bin/gzip"],
    "checksum": ["/usr/bin/sha256sum"],
}

def run_tool(tool_name: str, target_file: str) -> str:
    if tool_name not in ALLOWED_COMMANDS:
        raise ValueError(f"Tool not allowed: {tool_name}")
    cmd = ALLOWED_COMMANDS[tool_name] + [target_file]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, check=True)
    return result.stdout

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

class SubprocessModule(QueryType):
    fqns = ["subprocess"]


@python_rule(
    id="PYTHON-LANG-SEC-020",
    name="Dangerous subprocess Usage",
    severity="HIGH",
    category="lang",
    cwe="CWE-78",
    tags="python,subprocess,command-injection,OWASP-A03,CWE-78",
    message="subprocess call detected. Ensure arguments are not user-controlled.",
    owasp="A03:2021",
)
def detect_subprocess():
    """Detects subprocess module calls."""
    return SubprocessModule.method("call", "check_call", "check_output",
                                   "run", "Popen", "getoutput", "getstatusoutput")
```

## How to Fix

- Always use subprocess with a list of arguments and shell=False to prevent shell metacharacter interpretation.
- Validate all user-controlled values that become subprocess arguments against a strict allowlist or regex before use.
- Use absolute paths for executables to prevent PATH hijacking attacks.
- Set a timeout on all subprocess calls to prevent resource exhaustion from hung child processes.
- Capture stdout and stderr explicitly to prevent information leakage and ensure proper error handling.

## Security Implications

- **Command Injection via shell=True:** When shell=True is combined with a string that includes user input, shell metacharacters
in the input can inject additional commands. This is equivalent to calling os.system()
and is the most common subprocess-related vulnerability.

- **Arbitrary Executable via First Argument:** If the first element of the command list (the executable path) is attacker-controlled,
any program on the system can be run. This can be exploited via path traversal or by
pointing to attacker-placed binaries.

- **Argument Injection:** Even with shell=False, attacker-controlled values in the argument list can inject
unintended flags. For example, if a filename from user input starts with "-", it may
be interpreted as a command-line option by the target program (e.g., rsync, git, ffmpeg).

- **Sensitive Output Exposure:** subprocess commands may output sensitive information. If stdout is not captured, the
output appears in server logs or the terminal, potentially exposing credentials, system
configuration, or private data.


## FAQ

**Q: Is subprocess.run() with a list always safe?**

subprocess.run() with a list and shell=False is safe from shell injection since no shell
interprets the arguments. However, it is not safe from argument injection (flags injected
via filenames starting with "-") or from running attacker-controlled executables. Always
validate argument values and use absolute executable paths.


**Q: When is subprocess with shell=True acceptable?**

subprocess with shell=True is acceptable only when the entire command string is a
hardcoded literal with no user-controlled components. If any part of the command comes
from external input, even after validation, prefer shell=False with a list of arguments
to eliminate the shell injection risk entirely.


**Q: What is argument injection and how do I prevent it?**

Argument injection occurs when a value in the subprocess argument list starts with "-"
and is interpreted as a flag by the target program. For example, a filename of
"--checkpoint-action=exec=evil.sh" passed to tar would inject a malicious option.
Prevent this by validating that arguments do not start with "-" or by using "--" to
signal the end of options before file arguments (e.g., ["tar", "-czf", "archive.tar", "--", filename]).


**Q: Does shlex.quote() make shell=True safe?**

shlex.quote() provides some protection for POSIX shells but is not a complete solution.
It handles most metacharacters but may miss edge cases in specific shells or contexts.
The robust solution is to use shell=False with a list of arguments and avoid shell=True
entirely when user input is involved.


**Q: How should I handle subprocess errors and timeouts?**

Use check=True to raise CalledProcessError on non-zero exit codes, and always set a
timeout parameter. Catch subprocess.TimeoutExpired to terminate the child process and
handle the error gracefully. Use capture_output=True to prevent output from appearing
in server logs. Log the exit code and error output for security monitoring.


**Q: What is the difference between subprocess.run() and subprocess.Popen()?**

subprocess.run() is a high-level wrapper that blocks until the subprocess completes and
returns a CompletedProcess object. subprocess.Popen() is lower-level and provides more
control for streaming I/O, bidirectional communication, and async process management.
For most use cases, subprocess.run() with appropriate parameters is sufficient and safer.


## References

- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
- [Python docs: subprocess module](https://docs.python.org/3/library/subprocess.html)
- [OWASP OS Command Injection Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
- [OWASP Top 10 A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/)
- [Python subprocess security considerations](https://docs.python.org/3/library/subprocess.html#security-considerations)

---

Source: https://codepathfinder.dev/registry/python/lang/PYTHON-LANG-SEC-020
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
