What are the security risks of `pickle`, `eval`, and `exec`, and how do you avoid them?

7 minadvancedsecuritypickleevalexec

Quick Answer

pickle.loads() on untrusted data can execute arbitrary code during deserialization (a pickled object's __reduce__ can call any function), making it as dangerous as eval() on untrusted input. Never unpickle data from a source you don't fully trust. eval/exec run arbitrary Python source directly and should essentially never be used on user-supplied input. Use json/ast.literal_eval for safe parsing, and safer serialization formats (JSON, msgpack, protobuf) for data exchange across a trust boundary.

Detailed Answer

Why unpickling untrusted data is a full remote-code-execution risk

import pickle
import os

class Exploit:
    def __reduce__(self):
        return (os.system, ("echo pwned; rm -rf /tmp/demo",))

payload = pickle.dumps(Exploit())

# Anywhere this runs on untrusted input:
pickle.loads(payload)   # actually executes os.system(...) during unpickling!

__reduce__ is a legitimate protocol pickle uses to know how to reconstruct an object, but it can name any callable, and unpickling calls it. There is no way to "sandbox" pickle.loads() against a maliciously crafted payload. The official docs state plainly: never unpickle data received from an untrusted or unauthenticated source. This is the reason cache backends, message queues, or APIs that use pickle for convenience are a known attack surface if any external input can reach them.

eval()/exec(): running arbitrary source directly

user_input = "__import__('os').system('rm -rf /')"
eval(user_input)   # executes it -- catastrophic if user_input is attacker-controlled

eval (expressions) and exec (statements) execute Python source text directly, with the full power of the language. Passing any externally-influenced string to either is effectively giving that input author full code execution in your process.

The safe alternatives

import json
import ast

# Safe: parsing structured data
data = json.loads(user_json_string)              # only produces JSON-compatible values

# Safe: parsing a Python LITERAL (not arbitrary code)
value = ast.literal_eval("[1, 2, {'a': True}]")    # only literals -- no function calls, no imports
ast.literal_eval("__import__('os').system('x')")     # raises ValueError -- not a literal, rejected

json.loads only ever produces plain data (dicts, lists, strings, numbers, booleans, None). It cannot execute anything. ast.literal_eval is a genuinely safe, restricted subset of eval that parses only Python literals (numbers, strings, tuples, lists, dicts, booleans, None) and explicitly rejects anything resembling a function call or attribute access.

For serialization across a trust boundary, avoid pickle entirely

# Instead of pickling to send data between services / store in a shared cache:
import json
data = json.dumps({"user_id": 1, "action": "login"})

# For richer/faster binary serialization with the same "no code execution" safety:
# msgpack, protobuf, or a schema-validated format (pydantic models -> JSON)

pickle is appropriate only for trusted, same-process or same-organization data you fully control (caching your own computed Python objects to local disk, for example), never for data crossing a trust boundary (received from a network request, a third-party queue, user uploads, or any source you don't fully control end to end).

pickle.loads() on untrusted data is equivalent to arbitrary code execution, because a crafted payload's __reduce__ can invoke any callable during deserialization. Never unpickle data you don't fully trust. eval/exec on any externally-influenced string is the same class of risk. Use json/ ast.literal_eval for safe parsing, and JSON/msgpack/protobuf instead of pickle for any data that crosses a trust boundary.