# PYTHON-DJANGO-SEC-021: Django Code Injection via exec()

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

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

## Description

This rule detects code injection vulnerabilities in Django applications where untrusted
user input from HTTP request parameters flows into Python's exec() function.

Python's exec() executes its string argument as arbitrary Python statements, including
imports, function definitions, class definitions, assignments, and any other Python code.
Unlike eval() which is limited to expressions, exec() can execute complete programs.
When user-controlled data reaches exec(), an attacker can inject any Python code to
execute with the full privileges of the application process.

exec() is more powerful than eval() because it accepts statements (not just expressions),
enabling attackers to define persistent functions, import modules, modify global state,
and execute multi-line attack payloads. The same sandbox escape techniques that break
eval() restrictions also apply to exec().

Django applications should never call exec() with user-controlled input. If dynamic
code execution is genuinely needed for a use case, it should be implemented in a
sandboxed subprocess with no access to application secrets or the database.


## Vulnerable Code

```python
expr = request.GET.get('expr')
    result = eval(expr)
    return result


# SEC-021: exec with request data
def vulnerable_exec(request):
    code = request.POST.get('code')
    exec(code)


    func_name = request.GET.get('func')
    func = globals().get(func_name)
    return func()
```

## Secure Code

```python
from django.http import JsonResponse

# SECURE: Use an explicit allowlist dispatch instead of exec()
def list_items(request):
    from myapp.models import Item
    items = Item.objects.all().values('id', 'name')
    return JsonResponse({'items': list(items)})

def search_items(request):
    query = request.GET.get('q', '')
    from myapp.models import Item
    items = Item.objects.filter(name__icontains=query).values('id', 'name')
    return JsonResponse({'items': list(items)})

ALLOWED_ACTIONS = {
    'list': list_items,
    'search': search_items,
}

def dispatch_action(request):
    action = request.GET.get('action', '')
    # SECURE: Map string action names to pre-defined functions, no exec() needed
    handler = ALLOWED_ACTIONS.get(action)
    if handler is None:
        return JsonResponse({'error': 'Unknown action'}, status=400)
    return handler(request)

def run_analysis(request):
    # SECURE: If dynamic code execution is truly needed, run in isolated subprocess
    import subprocess
    script_name = request.POST.get('script', '')
    ALLOWED_SCRIPTS = {'report_daily', 'report_weekly', 'cleanup_temp'}
    if script_name not in ALLOWED_SCRIPTS:
        return JsonResponse({'error': 'Unknown script'}, status=400)
    result = subprocess.run(
        ['python', f'scripts/{script_name}.py'],
        capture_output=True, text=True, timeout=60
    )
    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 Builtins(QueryType):
    fqns = ["builtins"]

_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-021",
    name="Django Code Injection via exec()",
    severity="CRITICAL",
    category="django",
    cwe="CWE-95",
    tags="python,django,code-injection,exec,OWASP-A03,CWE-95",
    message="User input flows to exec(). Never use exec() with untrusted data.",
    owasp="A03:2021",
)
def detect_django_exec_injection():
    """Detects Django request data flowing to exec()."""
    return flows(
        from_sources=_DJANGO_SOURCES,
        to_sinks=[
            Builtins.method("exec").tracks(0),
            calls("exec"),
        ],
        sanitized_by=[],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Remove all exec() calls that process user-controlled input; replace with explicit allowlist dispatch patterns using pre-defined functions.
- If dynamic behavior is needed, encode it as data (configuration, feature flags, rule sets) rather than as executable code from user input.
- For plugin or extension systems, load code from trusted, authenticated sources on the filesystem, never from HTTP request parameters.
- Use Django's URL routing, middleware, and signal systems for dynamic dispatch rather than exec() or eval().
- If untrusted code must be executed for testing or educational purposes, use a fully isolated subprocess with no access to application secrets, a read-only filesystem, and network isolation.

## Security Implications

- **Full Remote Code Execution with Persistent State:** exec() can define functions, modify global variables, and inject persistent
code into the application's runtime. An attacker could redefine Django view
functions, add malicious middleware, or modify model methods -- changes that
persist until the process restarts.

- **Multi-Statement Attack Payloads:** Unlike eval() which is limited to single expressions, exec() accepts multi-line
Python code including loops, conditional blocks, and exception handlers. This
allows attackers to write sophisticated attack scripts that can probe the
environment, conditionally exfiltrate data, and cover their tracks.

- **Module Import and Capability Expansion:** exec() can execute import statements, giving attackers access to any Python
module available in the environment. exec("import socket; socket.connect(...)") or
exec("import subprocess; subprocess.run(...)") are trivial attack payloads once
exec() with user input is reachable.

- **Data Corruption and Database Manipulation:** With access to Django's ORM through exec(), an attacker can query, modify, or
delete any database record. They can also access Django's settings module to
extract database credentials and use them to connect directly to the database
outside the application.


## FAQ

**Q: How does exec() injection differ from eval() injection?**

eval() is limited to Python expressions (things that produce a value), while
exec() executes arbitrary statements including import, def, class, for, while,
and assignment statements. exec() injection is therefore more powerful: attackers
can define persistent functions, import modules, and execute multi-line programs.
Both are critical vulnerabilities; exec() is slightly worse due to the statement-
level access it provides.


**Q: We use exec() to implement a scripting feature in our Django admin. How should we redesign it?**

Options from safest to most complex: (1) Replace the scripting feature with a
configuration-based system where users configure behavior via structured data
(JSON rules, feature flags) rather than code. (2) Restrict available operations
to a fixed set of pre-defined actions users can combine. (3) If code execution
is truly required, run scripts as separate isolated subprocess calls with no
access to the application's Django process, environment, or database credentials.


**Q: Can adding try/except around exec() prevent the vulnerability?**

No. try/except catches exceptions but the injected code already executes before
any exception could be raised. An attacker can write code that produces no
exception while still reading secrets, making outbound network connections, or
modifying application state. Exception handling is not a security control for
code injection.


**Q: What about using exec() only with code from the database, not from request parameters?**

Database-stored code is a form of second-order injection. If an attacker can
write to the database through another vulnerability (e.g., insufficient access
controls, another injection vulnerability), they can plant malicious code that
exec() later executes. Never execute code from the database with exec() or eval().


**Q: Is there a performance reason teams use exec() that a safer approach would match?**

exec() is sometimes used for dynamic code generation for performance optimization
(e.g., NumPy uses it internally for generated C extensions). In web applications,
this use case almost never applies to user-facing request handling. If exec() is
used for code generation from static templates, ensure the templates never include
user-controlled values in code positions.


**Q: How does this finding affect our security audit or penetration test results?**

exec() with user input is one of the most severe findings a penetration test can
report -- it is Direct Remote Code Execution. It will always be classified as
Critical severity. Any audit, compliance review, or bug bounty program will treat
it as an immediate remediation priority that blocks deployment.


## References

- [CWE-95: Eval Injection](https://cwe.mitre.org/data/definitions/95.html)
- [OWASP Code Injection](https://owasp.org/www-community/attacks/Code_Injection)
- [OWASP Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html)
- [Python exec() documentation](https://docs.python.org/3/library/functions.html#exec)
- [Django Security](https://docs.djangoproject.com/en/stable/topics/security/)
- [Python sandbox escape techniques](https://book.hacktricks.xyz/generic-methodologies-and-resources/python/bypass-python-sandboxes)

---

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