# PYTHON-LANG-SEC-002: Dangerous exec() Usage Detected

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

- **Language:** Python
- **Category:** Python Core
- **URL:** https://codepathfinder.dev/registry/python/lang/PYTHON-LANG-SEC-002
- **Detection:** `pathfinder scan --ruleset python/PYTHON-LANG-SEC-002 --project .`

## Description

Python's built-in exec() function executes dynamically constructed Python code including
full statements, class and function definitions, import statements, and control flow. Unlike
eval(), exec() is not restricted to expressions and can execute any valid Python code,
making it significantly more dangerous.

When exec() receives a string derived from untrusted input such as HTTP parameters, file
contents, environment variables, or inter-process messages, an attacker can inject arbitrary
Python statements that spawn shells, exfiltrate data, install backdoors, modify the global
state of the application, or execute any OS command on the host system.

There is no safe way to sandbox exec() using restricted namespace dictionaries — Python's
introspection capabilities have been used to escape all known namespace-based sandboxes.
The correct fix is always to avoid exec() on untrusted input entirely.


## Vulnerable Code

```python
import code
import importlib
import typing

# SEC-002: exec
code_str = "print('hello')"
exec(code_str)
```

## Secure Code

```python
import ast
import types

# SECURE: Use ast.literal_eval() for safe literal parsing
def parse_literal(user_input: str):
    return ast.literal_eval(user_input)

# SECURE: Use explicit plugin loading via importlib with allowlist
ALLOWED_PLUGINS = {"plugin_a", "plugin_b", "plugin_c"}

def load_plugin(plugin_name: str):
    if plugin_name not in ALLOWED_PLUGINS:
        raise ValueError(f"Plugin not allowed: {plugin_name}")
    import importlib
    return importlib.import_module(f"myapp.plugins.{plugin_name}")

# SECURE: Use getattr() with an allowlist for dynamic method dispatch
ALLOWED_ACTIONS = {"start", "stop", "status"}

def run_action(obj, action: str):
    if action not in ALLOWED_ACTIONS:
        raise ValueError(f"Action not allowed: {action}")
    return getattr(obj, action)()

```

## 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-LANG-SEC-002",
    name="Dangerous exec() Detected",
    severity="HIGH",
    category="lang",
    cwe="CWE-95",
    tags="python,exec,code-injection,OWASP-A03,CWE-95",
    message="exec() detected. Never execute dynamically constructed code.",
    owasp="A03:2021",
)
def detect_exec():
    """Detects exec() calls."""
    return Builtins.method("exec")
```

## How to Fix

- Never pass user-controlled strings to exec(); there is no safe way to sandbox exec() on arbitrary input.
- Replace exec()-based plugin systems with importlib.import_module() restricted to an allowlist of module names.
- Use getattr() with explicit allowlists for dynamic method dispatch instead of constructing and exec()-ing code strings.
- If code generation is genuinely required, generate code at build time (not runtime) from trusted templates with no user-controlled content.
- Apply process isolation such as containers or separate processes with minimal privileges to limit the blast radius of any exec() exploitation.

## Security Implications

- **Full Remote Code Execution:** exec() can execute any valid Python statement, giving an attacker complete control over
the process. This includes defining functions, spawning threads, importing modules,
modifying global variables, and executing OS commands via os.system() or subprocess.

- **Application State Corruption:** exec() can redefine functions, classes, and module-level variables at runtime. An
attacker injecting code via exec() can replace authentication logic, disable security
checks, or corrupt application state in ways that persist for the lifetime of the process.

- **Namespace Sandbox Escape:** Attempts to restrict exec() by passing custom globals/locals dictionaries are
insufficient. Python's __subclasses__(), __mro__, and __builtins__ attributes provide
paths to escape any namespace restriction that has been attempted in practice.

- **Persistence and Lateral Movement:** Through exec(), an attacker can write files to disk, modify installed packages,
schedule cron jobs, and open persistent reverse shell connections that survive beyond
the current request or session.


## FAQ

**Q: Is exec() more dangerous than eval()?**

Yes. eval() is restricted to single expressions while exec() can execute any valid
Python code including import statements, function definitions, class definitions, and
multi-line programs. exec() is essentially a full Python interpreter invocation on
an arbitrary string.


**Q: Can I make exec() safe by restricting the globals and locals dictionaries?**

No. Namespace-based sandboxes for exec() have been broken repeatedly. Python's
introspection features such as __subclasses__(), object.__mro__, and __builtins__
provide multiple escape paths from any namespace restriction. Treat exec() on
untrusted input as unconditionally exploitable.


**Q: What should I use instead of exec() for dynamic code loading?**

Use importlib.import_module() with a strict allowlist of permitted module names for
plugin systems. Use getattr() with allowlists for dynamic method dispatch. Use
ast.literal_eval() for data parsing. Use template engines for code generation that
runs at build time rather than runtime.


**Q: Does this rule flag exec() used in legitimate build tools or metaprogramming?**

Yes. The rule flags all exec() call sites. Build tools that use exec() on trusted,
version-controlled code files are generally safe, but should be reviewed to ensure no
user-controlled data can influence the executed string. Document suppressed findings
with a clear explanation of the trust boundary.


**Q: What is the risk of exec() in Jupyter notebooks or interactive shells?**

In a Jupyter notebook or interactive shell, exec() is less risky because the user
already has code execution. However, any server-side component that uses exec() to
execute notebook cell content received from a client is critically dangerous and should
use a proper sandboxed execution environment instead.


**Q: How does Code Pathfinder detect taint flowing into exec()?**

The rule performs both pattern-based detection of all exec() calls and taint-aware
analysis that traces data flow from HTTP request parameters, os.environ, file reads,
and network input through variable assignments across function boundaries to the exec()
call site. Tainted flows are reported with higher severity.


## References

- [CWE-95: Eval Injection](https://cwe.mitre.org/data/definitions/95.html)
- [Python docs: built-in exec()](https://docs.python.org/3/library/functions.html#exec)
- [OWASP Code Injection](https://owasp.org/www-community/attacks/Code_Injection)
- [OWASP Top 10 A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/)
- [Python Security: Breaking Out of exec() Sandboxes](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html)

---

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