# PYTHON-DJANGO-SEC-020: Django Code Injection via eval()

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

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

## Description

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

Python's eval() interprets its string argument as a Python expression and evaluates it
in the current scope. When user-controlled data reaches eval(), an attacker can inject
arbitrary Python code that executes with the full privileges and scope of the application
process. Unlike SQL injection or command injection, eval() injection gives attackers
direct access to the Python runtime, all imported modules, the filesystem, and network
resources without any shell intermediary.

Even supposedly "safe" uses of eval() with restricted builtins or custom namespaces
have repeatedly been bypassed through creative use of Python's object model and
dunder attributes. There is no safe way to call eval() on untrusted input; the
function must be replaced with purpose-specific parsers (ast.literal_eval() for data
structures, or custom validators for specific expression types).


## Vulnerable Code

```python
# SEC-020: eval with request data
def vulnerable_eval(request):
    expr = request.GET.get('expr')
    result = eval(expr)
    return result


    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
import ast
import operator

# Safe arithmetic using AST validation
SAFE_NODES = (
    ast.Expression, ast.BinOp, ast.UnaryOp, ast.Constant,
    ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.USub,
)
SAFE_OPS = {
    ast.Add: operator.add, ast.Sub: operator.sub,
    ast.Mult: operator.mul, ast.Div: operator.truediv,
}

def eval_tree(node):
    if isinstance(node, ast.Expression):
        return eval_tree(node.body)
    elif isinstance(node, ast.Constant):
        return node.value
    elif isinstance(node, ast.BinOp):
        return SAFE_OPS[type(node.op)](eval_tree(node.left), eval_tree(node.right))
    raise ValueError(f"Unsupported node: {type(node)}")

def calculate(request):
    expression = request.GET.get('expr', '')
    # SECURE: Parse AST, validate only safe nodes, evaluate manually
    try:
        tree = ast.parse(expression, mode='eval')
        if not all(isinstance(n, SAFE_NODES) for n in ast.walk(tree)):
            raise ValueError("Unsafe expression")
        result = eval_tree(tree)
    except (ValueError, SyntaxError, KeyError):
        return JsonResponse({'error': 'Invalid expression'}, status=400)
    return JsonResponse({'result': result})

def parse_config(request):
    config_str = request.POST.get('config', '')
    # SECURE: ast.literal_eval() only evaluates Python literals safely
    try:
        config = ast.literal_eval(config_str)
        if not isinstance(config, dict):
            raise ValueError("Config must be a dict")
    except (ValueError, SyntaxError):
        return JsonResponse({'error': 'Invalid config format'}, status=400)
    return JsonResponse({'parsed': config})

```

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

## How to Fix

- Remove all uses of eval() with user-controlled input; there is no safe sanitizer for this.
- Use ast.literal_eval() for safely parsing Python literals (dicts, lists, strings, numbers) without executing arbitrary code.
- For mathematical expressions, implement a custom recursive descent parser over the AST using ast.parse() with strict node type validation.
- For function dispatch, use an explicit allowlist dictionary mapping string names to callable objects rather than using eval() or globals().
- For JSON-like data structures, use json.loads() which is safe and standardized.

## Security Implications

- **Direct Remote Code Execution:** eval() with user input is direct Remote Code Execution. An attacker can import
os, read filesystem contents, spawn shells, exfiltrate secrets, and install
persistence backdoors through a single request. No privilege escalation step
is needed -- the code runs immediately in the application process context.

- **Secret and Credential Theft:** Django applications store database passwords, API keys, and secret keys in
settings or environment variables. An injected expression like
__import__('os').environ can exfiltrate all of these in a single request.

- **Complete Application Compromise:** Beyond reading secrets, an attacker can modify application state, alter database
records, delete files, corrupt the application's module cache, or replace
functions with malicious versions that persist for the lifetime of the process.

- **Sandbox Escape Patterns:** Restricted namespaces and custom builtins passed to eval() do not provide
meaningful protection. Attackers can access the full Python object hierarchy
through patterns like ().__class__.__base__.__subclasses__() to obtain references
to arbitrary classes and modules without needing direct imports.


## FAQ

**Q: Is eval() ever safe to use with restricted builtins or namespaces?**

No. Passing eval() a restricted globals or locals dict does not prevent
sandbox escapes. Python's rich object model allows attackers to traverse the
class hierarchy using __class__, __bases__, __subclasses__, and __init__ to
access built-in functions and modules without direct name access. Multiple
CVEs exist for products that attempted restricted eval() sandboxes and were
bypassed. The only safe use of eval() is with fully static, hardcoded strings.


**Q: What is the difference between eval() and exec()? Are both flagged?**

eval() evaluates a single expression and returns its value. exec() executes
statements and does not return a meaningful value. Both interpret user input as
Python code when given untrusted strings and are equally dangerous. SEC-020
covers eval(), SEC-021 covers exec(). Both should be flagged and remediated.


**Q: Our application uses eval() for a math calculator feature. What should I do?**

Implement a safe math evaluator using ast.parse() and AST node validation.
Parse the input to an AST, walk all nodes, raise an error if any node type
is not in an allowlist of safe arithmetic nodes (BinOp, UnaryOp, Constant,
safe operator types), then recursively evaluate only the validated AST nodes.
The secure_example in this rule demonstrates this pattern.


**Q: Can ast.literal_eval() replace eval() for most use cases?**

ast.literal_eval() safely evaluates Python literals: strings, bytes, numbers,
tuples, lists, dicts, sets, booleans, and None. It raises ValueError or
SyntaxError for anything that is not a literal. It is safe for parsing
configuration values, coordinate pairs, or other structured data. It cannot
evaluate expressions, function calls, or variable references.


**Q: What if the eval() is inside a try/except block? Does that make it safe?**

No. Wrapping eval() in try/except catches exceptions but does not prevent code
execution. The attacker's injected code runs and any non-exception side effects
(file reads, network calls, process spawning) still occur before any exception
is raised or caught.


**Q: How severe is eval() injection compared to SQL injection?**

eval() injection is generally more severe. SQL injection is limited to database
operations (unless the database has OS-level features). eval() injection runs
arbitrary Python code in the application process directly, with access to all
imported modules, all environment variables, the filesystem, and network. It is
effectively Remote Code Execution with no intermediate step.


## References

- [CWE-95: Eval Injection](https://cwe.mitre.org/data/definitions/95.html)
- [OWASP Code Injection](https://owasp.org/www-community/attacks/Code_Injection)
- [Python ast.literal_eval() documentation](https://docs.python.org/3/library/ast.html#ast.literal_eval)
- [Django Security](https://docs.djangoproject.com/en/stable/topics/security/)
- [OWASP Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html)
- [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-020
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
