# PYTHON-FLASK-SEC-004: Flask Code Injection via eval()

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

- **Language:** Python
- **Category:** Flask
- **URL:** https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-004
- **Detection:** `pathfinder scan --ruleset python/PYTHON-FLASK-SEC-004 --project .`

## Description

This rule detects eval injection in Flask applications where user-controlled input
from HTTP request parameters reaches Python's built-in eval() function. eval()
evaluates its argument as a Python expression and returns the result. When the
argument contains attacker-controlled data, the attacker can inject arbitrary
Python expressions including function calls, attribute access, and import
statements that lead to remote code execution.

Unlike OS command injection, eval injection executes within the Python interpreter
itself. The attacker has direct access to Python builtins, imported modules, and
the application's own namespaces. Common payloads access __import__('os').system()
or use __builtins__ to invoke any Python functionality available in the process.

The taint analysis traces data from Flask request sources through variable
assignments and function calls to the eval() sink at argument position 0. The
.tracks(0) parameter means only the expression string argument is tracked --
the optional globals and locals dictionaries at positions 1 and 2 are not
analyzed by this rule. Flows through ast.literal_eval() or json.loads() are
recognized as sanitizers because these functions evaluate only safe literal
types (strings, numbers, lists, dicts, tuples, booleans, None) without
executing arbitrary code.


## Vulnerable Code

```python
from flask import Flask, request

app = Flask(__name__)

@app.route('/calc')
def calculator():
    expr = request.args.get('expr')
    result = eval(expr)
    return str(result)
```

## Secure Code

```python
from flask import Flask, request
import ast
import json

app = Flask(__name__)

@app.route('/parse')
def parse_value():
    value = request.args.get('value', '')
    # SAFE: ast.literal_eval() evaluates only Python literals
    # It raises ValueError for anything that is not a string, number,
    # tuple, list, dict, set, boolean, or None
    try:
        result = ast.literal_eval(value)
    except (ValueError, SyntaxError):
        return {'error': 'Invalid literal'}, 400
    return {'result': result}

@app.route('/config', methods=['POST'])
def load_config():
    raw = request.get_data(as_text=True)
    # SAFE: json.loads() for structured data -- JSON has no callable expressions
    try:
        config = json.loads(raw)
    except json.JSONDecodeError:
        return {'error': 'Invalid JSON'}, 400
    return {'loaded': True}

```

## 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"]


@python_rule(
    id="PYTHON-FLASK-SEC-004",
    name="Flask Code Injection via eval()",
    severity="CRITICAL",
    category="flask",
    cwe="CWE-95",
    tags="python,flask,code-injection,eval,rce,OWASP-A03,CWE-95",
    message="User input flows to eval(). Use ast.literal_eval() for safe evaluation.",
    owasp="A03:2021",
)
def detect_flask_eval_injection():
    """Detects Flask request data flowing to eval()."""
    return flows(
        from_sources=[
            calls("request.args.get"),
            calls("request.form.get"),
            calls("request.values.get"),
            calls("request.get_json"),
        ],
        to_sinks=[
            Builtins.method("eval").tracks(0),
            calls("eval"),
        ],
        sanitized_by=[
            calls("ast.literal_eval"),
            calls("json.loads"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Replace eval() with ast.literal_eval() when you need to parse Python literals (numbers, strings, lists, dicts) -- it rejects any expression that is not a safe literal type.
- Use json.loads() for structured data exchange instead of eval() -- JSON is a strict subset with no callable constructs.
- If you need to evaluate mathematical expressions, use a dedicated safe math library like simpleeval or numexpr instead of Python's eval().
- Never pass eval() a default globals or locals dict that contains application internals -- attackers can walk the object graph to reach sensitive state.
- Audit all uses of eval() in the codebase regardless of whether they currently receive user input -- refactor them out before new code paths introduce tainted data.

## Security Implications

- **Direct Remote Code Execution in the Python Interpreter:** eval() has access to Python builtins by default. An attacker can call
__import__('subprocess').check_output(['id']) to execute OS commands,
__import__('socket') to establish network connections, or access the
application's database session objects via the interpreter's global namespace.
There is no sandbox -- the attacker operates with the full privileges of the
Flask process.

- **Secret and Credential Extraction:** The Python runtime holds application secrets in memory: database passwords,
API keys, JWT secrets, and OAuth tokens. An injected expression like
globals()['app'].config['SECRET_KEY'] or os.environ['DB_PASSWORD'] extracts
these values and returns them in the HTTP response body.

- **Bypassing Authentication and Authorization:** eval() can call application functions directly. An attacker who knows the
codebase (from source code exposure or error messages) can invoke admin
functions, reset passwords, or elevate privileges by calling internal Python
functions through the eval() context.

- **Supply Chain Attacks via Injected Imports:** __import__() inside eval() can load any installed Python package. In
environments with broad pip dependencies, attackers use eval injection to
instantiate classes from installed libraries in unexpected ways, reaching
attack surface that is not directly accessible from the Flask route handlers.


## FAQ

**Q: Is ast.literal_eval() truly safe for all user input?**

ast.literal_eval() evaluates only Python literals: strings, bytes, numbers,
tuples, lists, dicts, sets, booleans, and None. It raises ValueError for
anything that contains a function call, attribute access, or import statement.
It is safe for parsing structured data but is not a general-purpose alternative
to eval() -- if you need math expressions or more complex evaluation, use a
dedicated safe expression library.


**Q: Can the attacker bypass ast.literal_eval() sanitization?**

No. ast.literal_eval() parses the input into an AST and then evaluates only
nodes that correspond to literal types. If the AST contains any non-literal
node, it raises ValueError before evaluation. There are no known bypasses.
The risk is calling ast.literal_eval() on the result of a manipulation that
turns a literal into an expression -- always apply it directly to the raw
user input.


**Q: How does this rule handle eval() calls inside class methods or nested functions?**

The taint analysis follows data flow regardless of call depth or class
structure. If request.args.get() feeds a value through a class method, a
decorator, or a nested function that eventually calls eval(), the rule traces
the full chain. Scope boundaries within the same Python module and across
imported modules are both handled.


**Q: What if I need a calculation engine for user-defined formulas?**

Use simpleeval (pip install simpleeval) for arithmetic expression evaluation.
It supports operators and basic math functions but restricts the expression
language to a safe subset. For spreadsheet-style formulas, formulaic is another
option. Both are designed specifically for safe user-defined expression
evaluation and do not expose Python's object model.


**Q: How do I fix an existing eval() call that processes user input?**

Start by determining what the input is supposed to contain. If it is a number,
use int() or float(). If it is a Python literal (list, dict, tuple), use
ast.literal_eval(). If it is JSON, use json.loads(). If it is a mathematical
expression, use simpleeval. If no safe alternative covers the use case, the
feature needs to be redesigned to not require arbitrary expression evaluation.


**Q: Will this rule flag eval() on hardcoded strings in tests?**

No. The rule only flags flows where the argument to eval() is reachable from
a Flask request source (request.args.get, etc.). eval('2 + 2') with a
hardcoded string literal does not create a tainted flow and will not be flagged.


**Q: Does this rule cover compile() as well?**

No. compile() followed by exec() is covered by PYTHON-FLASK-SEC-005, which
tracks flows to exec() and compile() separately. SEC-004 covers eval() only.
Run both rules together for complete coverage of Python code injection patterns.


## 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 eval() built-in documentation](https://docs.python.org/3/library/functions.html#eval)
- [Python ast.literal_eval documentation](https://docs.python.org/3/library/ast.html#ast.literal_eval)
- [Ned Batchelder: eval() really is dangerous](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html)
- [OWASP Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html)

---

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