Flask Code Injection via exec()

CRITICAL

User input from Flask request parameters flows to exec() or compile(). exec() cannot be safely sanitized -- redesign the feature to avoid dynamic code execution.

Rule Information

Language
Python
Category
Flask
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythonflaskcode-injectionexeccompilercecross-fileinter-proceduraltaint-analysisCWE-95OWASP-A03
CWE References

Interactive Playground

Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.

pathfinder scan --ruleset python/PYTHON-FLASK-SEC-005 --project .
1
2
3
4
5
6
7
8
9
rule.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

About This Rule

Understanding the vulnerability and how it is detected

This rule detects code injection in Flask applications where user-controlled input from HTTP request parameters reaches exec() or compile(). exec() is more dangerous than eval() because it executes full Python statements, not just expressions. It can define classes, import modules, modify global state, spawn threads, and run arbitrary multi-line code blocks. compile() generates a code object that is typically passed to exec() or eval() -- tainted input to compile() is a precursor to the same attack.

Unlike eval injection (PYTHON-FLASK-SEC-004), exec() injection has no safe sanitizer. ast.literal_eval() does not help because exec() accepts statements, not literals. Sandboxing exec() with restricted globals/locals dictionaries is notoriously difficult to get right -- numerous Python sandbox escapes exist that allow attackers to reach the unrestricted interpreter from a sandboxed exec() call.

The only correct fix is to eliminate exec() from code paths that process user input. This rule uses taint analysis to find those paths: it traces data from Flask request sources through assignments and function calls to exec() and compile() at argument position 0, flagging every reachable path regardless of how many intermediate steps are involved.

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

Unrestricted Python Statement Execution

exec() executes any valid Python statement. Unlike eval(), which is limited to expressions, exec() can run import statements, define and call functions, modify global and local variables, and execute multi-line code blocks. An attacker who controls exec() input has the same capabilities as a developer with write access to the codebase.

2

Persistent Backdoor Installation

exec() can write files, install packages (subprocess + pip), modify Python module caches, and alter loaded module objects in memory. An attacker can inject code that installs a backdoor into the application's module namespace, persisting across requests without touching the filesystem.

3

Complete Secret Extraction

exec() has access to the application's global namespace. Injected code can iterate globals(), find database connections, configuration objects, and in-memory caches containing session tokens and API keys, and exfiltrate them via an HTTP request made from within the exec() call.

4

Sandbox Escape

Attempts to restrict exec() by passing a limited globals dict are broken by well-known Python sandbox escapes: ().__class__.__mro__[1].__subclasses__() gives access to all loaded classes, from which file objects, socket objects, and subprocess handles can be obtained. There is no reliable way to sandbox exec() against a determined attacker.

How to Fix

Recommended remediation steps

  • 1Eliminate exec() entirely from any code path that can be reached with user-controlled input -- there is no sanitizer that makes exec(user_input) safe.
  • 2If you need user-defined computation, use a restricted expression language (simpleeval for math, jsonata for JSON transformations) rather than Python's full exec() surface.
  • 3If exec() is used to load configuration, replace it with a structured configuration format (YAML with yaml.safe_load(), TOML, JSON) that cannot execute code.
  • 4If exec() is used for plugin loading, switch to a proper plugin architecture (importlib.import_module() with a controlled plugin directory and signature verification).
  • 5Audit all exec() and compile() calls in the codebase and document the reason each exists -- any that process external input must be removed or redesigned.

Detection Scope

How Code Pathfinder analyzes your code for this vulnerability

Scope: global (cross-file taint tracking across the entire project). Sources: Flask HTTP input methods -- request.args.get(), request.form.get(), request.values.get(), request.get_json() -- all of which deliver attacker- controlled strings that may contain valid Python statement syntax. Sinks: exec() and compile(). The .tracks(0) parameter focuses on argument position 0, the code string. compile() at position 0 is included because it is the standard way to create a code object for subsequent exec() or eval() calls -- a tainted compile() call is a precursor to tainted execution. Sanitizers: None. There is no recognized sanitizer for exec() because no transformation of user input makes it safe to execute as Python code. Flows that pass through any intermediate function still trigger a finding unless the intermediate function completely replaces the value with a non-tainted result. The rule follows tainted values through assignments, return values, and cross-file function calls.

Compliance & Standards

Industry frameworks and regulations that require detection of this vulnerability

OWASP Top 10
A03:2021 - Injection
CWE Top 25
CWE-95 -- Dynamically Evaluated Code Injection
PCI DSS v4.0
Requirement 6.2.4 -- prevent injection attacks including code injection
NIST SP 800-53
SI-10: Information Input Validation; SA-15: Development Process Controls
ISO 27001
A.14.2.5 -- secure system engineering principles

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Flask Code Injection via exec()

eval() is limited to expressions. ast.literal_eval() can filter its input to safe literal types because literals are a well-defined subset of expressions. exec() accepts full Python statements -- imports, function definitions, loops, assignments -- and there is no meaningful subset of statements that is both useful to users and safe to execute. Python sandbox escapes allow attackers to reach unrestricted Python from any exec() call with limited globals.
Move plugins to the filesystem and load them with importlib.import_module(). Sign plugin files and verify signatures before loading. This gives you dynamic extensibility without passing user-HTTP-controlled strings to exec(). If plugins must come from the database, treat the plugin directory as trusted infrastructure and never allow HTTP requests to influence which plugin code gets stored.
Yes, if the source string passed to compile() is tainted. The rule tracks taint to compile() at argument position 0. If the template source comes from a static file or a trusted configuration store (not from HTTP input), the flow is not tainted and will not be flagged.
SEC-004 covers eval(), which executes Python expressions. SEC-005 covers exec() and compile(), which execute Python statements. exec() is strictly more powerful than eval() -- every expression is a valid statement but not vice versa. Run both rules to cover the complete dynamic code execution surface.
Authenticated endpoints are still reachable via stolen credentials, session fixation, or CSRF. exec() behind authentication reduces the attack surface but does not eliminate it. The rule will still flag authenticated endpoints because the authentication gate is not a sanitizer for the tainted value. Remove exec() from all HTTP-reachable code paths.
Only if the test creates a tainted flow from a Flask request source (e.g., via the test client) to exec(). Hardcoded exec('code_string') in test files does not create a tainted flow and is not flagged.
If there is a code path where exec() receives only values from a trusted, non-HTTP-controlled source that incorrectly appears tainted (e.g., a constant loaded from a config file that the analysis cannot distinguish from request data), you can add a # pathfinder: ignore PYTHON-FLASK-SEC-005 comment at the exec() call site with a written explanation.

New feature

Get these findings posted directly on your GitHub pull requests

The Flask Code Injection via exec() rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.

See how it works