# PYTHON-LANG-SEC-010: Dangerous os.system() or os.popen() Call

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

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

## Description

Python's os.system() and os.popen() functions execute a shell command string by invoking
the system shell (/bin/sh on Unix, cmd.exe on Windows). Because the command is passed through
a shell interpreter, special characters such as semicolons, pipes, backticks, dollar signs,
and redirections are interpreted as shell metacharacters.

When any part of the command string is derived from untrusted user input, an attacker can
inject additional commands using shell metacharacters. For example, user input of
"file.txt; rm -rf /" would cause os.system("cat file.txt; rm -rf /") to execute both
commands.

The recommended replacement is subprocess.run() with a list of arguments (not a shell string),
which bypasses the shell entirely and passes arguments directly to the OS exec() syscall.
subprocess.run(["cat", filename]) is safe regardless of what filename contains.


## Vulnerable Code

```python
import os

os.system("rm -rf /tmp/cache")
os.popen("ls -la")
```

## Secure Code

```python
import subprocess
import shlex

# SECURE: Use subprocess.run() with a list of arguments (no shell)
def process_file(filename: str) -> str:
    result = subprocess.run(
        ["cat", filename],
        capture_output=True,
        text=True,
        check=True,
        timeout=30,
    )
    return result.stdout

# SECURE: Validate and sanitize input before use in commands
import os
import re

SAFE_FILENAME_RE = re.compile(r'^[a-zA-Z0-9_\-\.]+$')

def convert_image(input_name: str, output_name: str) -> None:
    for name in (input_name, output_name):
        if not SAFE_FILENAME_RE.match(name):
            raise ValueError(f"Invalid filename: {name}")
    subprocess.run(
        ["convert", input_name, output_name],
        check=True,
        timeout=60,
    )

```

## Detection Rule (Python SDK)

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

class OSModule(QueryType):
    fqns = ["os"]


@python_rule(
    id="PYTHON-LANG-SEC-010",
    name="Dangerous os.system() Call",
    severity="HIGH",
    category="lang",
    cwe="CWE-78",
    tags="python,os-system,command-injection,OWASP-A03,CWE-78",
    message="os.system() detected. Use subprocess.run() with list arguments instead.",
    owasp="A03:2021",
)
def detect_os_system():
    """Detects os.system() and os.popen() calls."""
    return OSModule.method("system", "popen", "popen2", "popen3", "popen4")
```

## How to Fix

- Replace os.system() and os.popen() with subprocess.run() using a list of arguments, which bypasses the shell entirely.
- Never construct shell command strings by concatenating or interpolating user input, even if the input appears to be validated.
- If a shell pipeline is genuinely required, validate all user-supplied values against a strict allowlist before including them.
- Set a timeout on subprocess.run() to prevent resource exhaustion from long-running injected commands.
- Run the Python process with the minimum OS privileges required so that injected commands cannot escalate to root or access sensitive paths.

## Security Implications

- **OS Command Injection:** Shell metacharacters in user-supplied input allow injecting additional commands. An
attacker can chain arbitrary commands using semicolons, pipes, or newlines, executing
them with the privileges of the Python process.

- **Data Exfiltration:** Injected commands can read sensitive files (credentials, private keys, configuration),
encode them, and send them to an attacker-controlled endpoint using curl, wget, or
other available tools on the system.

- **Persistent Backdoor Installation:** Command injection can write cron jobs, SSH authorized_keys, or systemd unit files that
survive process restarts and provide persistent access to the attacker even after the
vulnerability is patched.

- **Lateral Movement:** On cloud environments or internal networks, injected commands can access metadata
services for IAM credential theft, scan internal networks, and pivot to other systems
using the compromised host's network access.


## FAQ

**Q: Is os.system() safe if I validate the input with a regex first?**

Regex validation reduces but does not eliminate the risk. Shell metacharacter sets vary
by shell and locale, and regex patterns often miss edge cases like null bytes, Unicode
lookalikes, or environment variable expansion. The safe approach is to use subprocess.run()
with a list so the shell is never invoked and metacharacter interpretation is impossible.


**Q: What is the difference between os.system() and subprocess.run() with shell=True?**

Both invoke the system shell and are equally vulnerable to command injection. The safe
alternative is subprocess.run() with a list of arguments and shell=False (the default),
which passes the arguments directly to the OS exec() syscall without shell interpretation.


**Q: Does os.popen() have the same risk as os.system()?**

Yes. os.popen() also passes the command to the shell and is vulnerable to the same
injection attacks. Additionally, os.popen() was deprecated in Python 2.6 in favor of
the subprocess module. There is no reason to use os.popen() in modern Python code.


**Q: How do I handle commands that require shell features like pipes or globbing?**

Use Python built-in equivalents: the glob module for file globbing, subprocess.PIPE for
connecting processes, and pathlib for path manipulation. If a shell pipeline is unavoidable,
use subprocess.run() with shell=True but ensure every element of the command string is
either a hardcoded literal or validated against a strict allowlist.


**Q: Will this rule flag os.system() called with a hardcoded string literal?**

Yes. All os.system() call sites are flagged as a security audit point. Hardcoded strings
are lower risk but may still be problematic if the hardcoded command includes values that
could be influenced by environment variables, file contents, or other runtime state.


**Q: What timeout should I use for subprocess.run() calls?**

Use a timeout appropriate to the expected execution time of the command, typically a few
seconds for file operations and up to a minute for external tools. Setting timeout=30 as
a default is a reasonable starting point. Always handle subprocess.TimeoutExpired to
terminate the child process cleanly.


## References

- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
- [Python docs: os.system()](https://docs.python.org/3/library/os.html#os.system)
- [Python docs: subprocess module](https://docs.python.org/3/library/subprocess.html)
- [OWASP Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
- [OWASP OS Command Injection Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)

---

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