# PYTHON-DJANGO-SEC-010: Django Command Injection via os.system()

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

- **Language:** Python
- **Category:** Django
- **URL:** https://codepathfinder.dev/registry/python/django/PYTHON-DJANGO-SEC-010
- **Detection:** `pathfinder scan --ruleset python/PYTHON-DJANGO-SEC-010 --project .`

## Description

This rule detects OS command injection vulnerabilities in Django applications where
untrusted user input from HTTP request parameters flows into os.system() calls.

os.system() executes its string argument through the system shell (typically /bin/sh
on Unix). When user-controlled data is embedded in the command string, an attacker
can inject shell metacharacters (semicolons, pipes, backticks, $() substitution)
to execute arbitrary commands. The injected commands run with the full privileges of
the Django application process, which on misconfigured servers may include file
system access, network access, or even root privileges.

os.system() is particularly dangerous because it always invokes the shell, unlike
subprocess.run() with a list argument which can bypass the shell entirely. There is
no safe way to use os.system() with user-controlled input; the function should be
replaced with subprocess.run() using a list of arguments.


## Vulnerable Code

```python
import os
import subprocess

# SEC-010: os.system with request data
def vulnerable_os_system(request):
    filename = request.GET.get('file')
    os.system(f"cat {filename}")


    cmd = request.POST.get('command')
    subprocess.call(cmd, shell=True)


    host = request.GET.get('host')
    proc = subprocess.Popen(f"ping {host}", shell=True)
    return proc.communicate()
```

## Secure Code

```python
from django.http import JsonResponse
import subprocess
import re

ALLOWED_COMMANDS = {'ping', 'nslookup', 'traceroute'}

def network_diagnostic(request):
    host = request.GET.get('host', '')
    command = request.GET.get('command', '')
    # SECURE: Allowlist the command and validate the host format
    if command not in ALLOWED_COMMANDS:
        return JsonResponse({'error': 'Unknown command'}, status=400)
    if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
        return JsonResponse({'error': 'Invalid host format'}, status=400)
    # SECURE: Use subprocess with a list, never shell=True with user input
    result = subprocess.run(
        [command, host],
        capture_output=True,
        text=True,
        timeout=10
    )
    return JsonResponse({'output': result.stdout})

def process_file(request):
    filename = request.POST.get('filename', '')
    # SECURE: Validate filename against allowlist or strict pattern
    if not re.match(r'^[a-zA-Z0-9_.\-]+$', filename):
        return JsonResponse({'error': 'Invalid filename'}, status=400)
    result = subprocess.run(
        ['file', '--', filename],
        capture_output=True,
        text=True
    )
    return JsonResponse({'type': 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 OSModule(QueryType):
    fqns = ["os"]

_DJANGO_SOURCES = [
    calls("request.GET.get"),
    calls("request.POST.get"),
    calls("request.GET"),
    calls("request.POST"),
    calls("request.COOKIES.get"),
    calls("request.FILES.get"),
    calls("*.GET.get"),
    calls("*.POST.get"),
]


@python_rule(
    id="PYTHON-DJANGO-SEC-010",
    name="Django Command Injection via os.system()",
    severity="CRITICAL",
    category="django",
    cwe="CWE-78",
    tags="python,django,command-injection,os-system,OWASP-A03,CWE-78",
    message="User input flows to os.system(). Use subprocess with list args and shlex.quote().",
    owasp="A03:2021",
)
def detect_django_os_system_injection():
    """Detects Django request data flowing to os.system()/os.popen()."""
    return flows(
        from_sources=_DJANGO_SOURCES,
        to_sinks=[
            OSModule.method("system", "popen", "popen2", "popen3", "popen4"),
        ],
        sanitized_by=[
            calls("shlex.quote"),
            calls("shlex.split"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Replace all os.system() calls with subprocess.run() using a list of arguments, which avoids shell interpretation entirely.
- Never use shell=True in subprocess calls when any argument originates from user input, as this reintroduces shell injection risk.
- Validate user input against strict allowlists (e.g., regex for hostname format, explicit set membership for command names) before passing to subprocess.
- Consider whether the functionality requiring shell commands can be achieved with Python standard library functions that don't invoke a shell at all.
- Run Django applications under a dedicated low-privilege user account and container security profiles (seccomp, AppArmor) to limit the impact of any successful injection.

## Security Implications

- **Arbitrary Command Execution:** An attacker who controls any part of the string passed to os.system() can execute
arbitrary commands on the server. Shell metacharacters like semicolons, pipes,
backticks, and $() let attackers chain additional commands after the intended one,
regardless of what comes before or after their input in the string.

- **Full Server Compromise:** Commands injected through os.system() run as the Django application user. In
containerized environments this is typically a low-privilege user, but in
misconfigured deployments it may be the web server user or even root. In either
case, an attacker can read application secrets, modify files, install backdoors,
or pivot to other internal network services.

- **Data Exfiltration via DNS or HTTP:** Even in restricted environments, attackers can use injected commands to exfiltrate
data through DNS lookups, outbound HTTP requests, or writing to files served by
the web application itself. Command injection in a restricted container can still
lead to credential theft and lateral movement.

- **Reverse Shell Establishment:** Attackers commonly use command injection to establish a reverse shell connection
back to attacker-controlled infrastructure, providing persistent interactive access
to the server even after the original vulnerability is patched.


## FAQ

**Q: Why is os.system() always unsafe with user input while subprocess can be safe?**

os.system() always passes its argument to the system shell for interpretation,
which means shell metacharacters in user input will always be interpreted.
subprocess.run(['command', 'arg']) with a list bypasses the shell entirely --
the list items are passed directly to execve() as argv, so metacharacters are
treated as literal characters, not shell syntax.


**Q: Is shlex.quote() sufficient to make os.system() safe?**

shlex.quote() makes a single argument safe for inclusion in a shell command string,
but it is still preferable to use subprocess with a list. If shlex.quote() is
misapplied (e.g., applied to only part of the command, or to the entire command
string rather than individual arguments), it may not protect against injection.
The safest approach is subprocess with a list and no shell=True.


**Q: Can this rule detect command injection through Django management commands?**

If a custom Django management command calls os.system() with user-influenced data
(e.g., from command arguments that originated in a web request stored in the
database), the rule will detect it if the taint flow is traceable. Management
commands that process database values should also be audited for injection since
stored XSS and second-order injection can originate from attacker-controlled
database records.


**Q: What if we need to run a specific set of system commands from user requests?**

Use an explicit allowlist: define a dictionary mapping safe command names to
the actual subprocess list arguments. Validate that the user's input matches
a key in the dictionary, then execute only the corresponding pre-defined command.
Never construct the command list from user input, even for allowlisted command names.


**Q: How do I handle file processing that requires calling system tools?**

Use subprocess with a pre-built list where the filename argument is validated
with a strict regex (alphanumeric plus safe characters only) and prefixed with
'--' to prevent it from being interpreted as a flag. For common file operations
like image conversion or document processing, use Python libraries (Pillow,
python-docx) instead of shelling out to system tools.


**Q: What compliance frameworks specifically require command injection protection?**

CWE-78 is #5 in the 2023 CWE Top 25. OWASP Top 10 A03:2021 covers injection
broadly. PCI DSS v4.0 Requirement 6.2.4 requires protection against injection
attacks. NIST SP 800-53 SI-10 requires input validation. Any SOC 2, ISO 27001,
or FedRAMP assessment will include command injection in penetration testing scope.


## References

- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
- [OWASP OS Command Injection Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
- [OWASP Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
- [Python subprocess documentation](https://docs.python.org/3/library/subprocess.html)
- [Django Security](https://docs.djangoproject.com/en/stable/topics/security/)
- [Python security considerations for subprocess](https://docs.python.org/3/library/subprocess.html#security-considerations)

---

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