Secure Python: deserialization, validation & secrets
pickle/yaml.load dangers, input validation, and the secrets module.
Python makes several dangerous things easy, and security tooling — which runs with access and privilege — is exactly where those footguns matter most. Deserialising the wrong format executes code. Trusting input without validation lets it reach places it should not. Generating "random" tokens with the wrong module makes them predictable. Each has a safe alternative that costs nothing to use.
Deserialization is remote code execution
pickle.load and yaml.load (with the default Loader) do not merely read data — they can construct arbitrary Python objects, which means loading an attacker-controlled payload runs attacker code. Never unpickle data you did not produce and protect, and always use yaml.safe_load for YAML. For data interchange, prefer JSON, which has no code-execution semantics. This is one of the most common critical Python CVEs, and it is entirely avoidable.
import yaml, json# DANGEROUS: full loader can instantiate arbitrary types -> RCEcfg = yaml.load(untrusted_text, Loader=yaml.Loader) # NEVER# SAFE: only plain scalars, lists, dictscfg = yaml.safe_load(untrusted_text)# DANGEROUS: unpickling untrusted bytes runs code during loadobj = pickle.loads(network_bytes) # NEVER# SAFE: use a data format with no execution semanticsobj = json.loads(network_bytes)
Validate input at the boundary
Treat everything crossing into your program — CLI args, file contents, API responses, environment — as untrusted until validated. Check types, ranges, and formats once at the edge, and reject early with a clear error, so the core logic only ever sees good data. pydantic makes this declarative: define the shape, and parsing enforces it. Path inputs deserve special care — resolve and confine them so a value like ../../etc/passwd cannot escape your intended directory.
from pydantic import BaseModel, field_validatorfrom pathlib import Pathclass Job(BaseModel):target: strcount: int@field_validator("count")@classmethoddef sane(cls, v):if not 1 <= v <= 1000: raise ValueError("count out of range")return vjob = Job.model_validate_json(raw) # raises on bad shape/values# path traversal defence: resolve and confine under a base dirbase = Path("/srv/data").resolve()p = (base / user_supplied).resolve()if not p.is_relative_to(base): # Python 3.9+raise ValueError("path escapes base directory")
Cryptographic randomness and secrets
The random module is a fast pseudo-random generator seeded predictably — perfect for simulations, catastrophic for anything security-sensitive. Tokens, passwords, nonces, and salts must come from the secrets module (or os.urandom), which draws from the OS CSPRNG. Compare secrets with secrets.compare_digest to avoid timing side channels, and reach for the cryptography library rather than hand-rolling any primitive.
import secrets# WRONG: predictable, seeded PRNG — do not use for anything security-related# token = "".join(random.choices(string.ascii_letters, k=32))# RIGHT: cryptographically securetoken = secrets.token_urlsafe(32) # URL-safe API tokenhex_key = secrets.token_hex(16) # 128-bit key material# constant-time comparison defeats timing attacks on secret equalityif secrets.compare_digest(provided, expected):grant_access()