Safeguard
DevSecOps

Why Python's eval() Is Dangerous and What to Use Instead

Python eval() runs arbitrary code, and feeding it untrusted input is a remote code execution bug waiting to happen. Here's the risk, how the exploit works conceptually, and safer alternatives.

Marcus Chen
DevSecOps Engineer
6 min read

Python eval() executes whatever string you hand it as live Python code, so passing it any input a user can influence is one of the most direct ways to hand an attacker remote code execution. The function itself is not a bug — it does exactly what it advertises. The danger is that developers reach for python eval to solve small parsing problems ("I just need to turn this string into a number") and unknowingly build a code-injection hole in the process. This guide explains why, shows the exploit class conceptually so you can recognize it in review, and gives you the alternatives that solve the real problem without the risk.

What eval() actually does

eval() takes a string, compiles it as a Python expression, and runs it in the current scope. So this:

result = eval("2 + 3 * 4")

returns 14. Convenient. The problem is that eval does not distinguish "arithmetic I expected" from "any expression at all." If the string comes from a request parameter, a config file an attacker can influence, or a message on a queue, you have handed them an interpreter.

Consider a naive calculator endpoint:

def calculate(expression: str):
    return eval(expression)  # never do this with user input

The author imagined inputs like "3 + 4". But an attacker sends an expression that calls into the standard library to read files, open network connections, or spawn a shell. Because eval runs with the same privileges as your process, whatever your service can do, the injected expression can do. This is the python eval exploit in one sentence: the input is not data, it is code, and the attacker writes the code.

Why "sandboxing" eval() doesn't work

The common next move is to try to lock eval down by restricting its namespaces:

eval(user_input, {"__builtins__": {}}, {})

This looks safer — you have stripped the builtins so there is no obvious open or __import__. It is not safe. Restricted-eval escapes are a well-documented cat-and-mouse game: attackers reach dangerous functionality through object introspection, walking from a harmless-looking object up its class hierarchy to reach subclasses that can execute code. Security researchers have published a long history of bypasses for every "safe eval" recipe. The consensus among Python security practitioners is blunt: you cannot reliably sandbox eval() against a determined attacker inside the same interpreter. Do not try. Remove the need for it instead.

The safe alternatives

Almost every legitimate use of eval() on untrusted input is actually one of a few narrower problems that have safe, purpose-built solutions.

You need to parse a literal (number, list, dict, string). Use ast.literal_eval. It evaluates only Python literal structures and refuses anything else — no function calls, no attribute access, no arbitrary code.

import ast

value = ast.literal_eval("[1, 2, 3]")   # works
ast.literal_eval("__import__('os')")    # raises ValueError

You need to parse data interchange. Use json.loads. If the input is meant to be structured data, JSON is a data format, not a code format, and its parser cannot execute anything.

import json
data = json.loads('{"count": 42}')

You need arithmetic from users. Parse the expression yourself with the ast module and walk the tree, allowing only the node types you intend (numbers and the arithmetic operators), or use a dedicated math-expression library that never touches the interpreter. This is more code than eval, but it is the difference between a calculator and an RCE.

You need dynamic attribute or method access by name. Use getattr with an explicit allowlist, not string-built code.

Finding eval() before it ships

eval and its cousin exec are exactly the kind of thing static analysis is good at catching. A linter such as Bandit flags eval usage by default (rule B307), and it belongs in your CI so a new call gets caught at review time rather than in an incident. Grep is a reasonable first pass:

grep -rn "eval(" --include="*.py" .

Treat every hit as a question: where does this string come from, and can an attacker reach it? Direct user input is an obvious red flag, but so is any value that transits a database, a cache, a config service, or a message broker an attacker might be able to write to.

The same category of risk shows up in the libraries you depend on, not just your own code. A dependency that uses eval or pickle on data it receives can expose you even if your code is clean, and an SCA tool such as Safeguard can flag when a package in your tree has a known code-injection or deserialization CVE. Static analysis of your own source is covered on our DAST and testing side, and the Academy has more on the injection class generally.

FAQ

Is eval() always unsafe?

eval() is safe only when the string is fully under your control and never influenced by any external input. The moment user input, file contents, or network data reaches it, treat it as remote code execution. In practice that is often enough reason to avoid it entirely and use a narrower tool.

What's the difference between eval() and ast.literal_eval()?

eval() runs any Python expression, including function calls and imports. ast.literal_eval() only evaluates literal structures — numbers, strings, tuples, lists, dicts, booleans, and None — and raises an error on anything else. For turning a string into data, literal_eval is the safe choice.

Can I make eval() safe by clearing __builtins__?

No. Restricting the namespace does not stop a determined attacker, who can reach dangerous functionality through object introspection. Sandboxing eval() inside the same interpreter is not considered reliable. Eliminate the need for eval rather than trying to contain it.

How do I catch eval() usage across a codebase?

Run a static analysis tool like Bandit in CI — it flags eval/exec automatically — and back it with a grep in pre-commit. Then review each occurrence for whether the evaluated string can be influenced from outside the process.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.