CoursesAdvanced scripting for DevSecOpsPython for security automation

Secure Python: deserialization, validation & secrets

pickle/yaml.load dangers, input validation, and the secrets module.

Expert35 min · lesson 12 of 15

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.

the deserialization footguns and fixes
import yaml, json
# DANGEROUS: full loader can instantiate arbitrary types -> RCE
cfg = yaml.load(untrusted_text, Loader=yaml.Loader) # NEVER
# SAFE: only plain scalars, lists, dicts
cfg = yaml.safe_load(untrusted_text)
# DANGEROUS: unpickling untrusted bytes runs code during load
obj = pickle.loads(network_bytes) # NEVER
# SAFE: use a data format with no execution semantics
obj = 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.

boundary validation + path confinement
from pydantic import BaseModel, field_validator
from pathlib import Path
class Job(BaseModel):
target: str
count: int
@field_validator("count")
@classmethod
def sane(cls, v):
if not 1 <= v <= 1000: raise ValueError("count out of range")
return v
job = Job.model_validate_json(raw) # raises on bad shape/values
# path traversal defence: resolve and confine under a base dir
base = 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.

secrets, not random
import secrets
# WRONG: predictable, seeded PRNG — do not use for anything security-related
# token = "".join(random.choices(string.ascii_letters, k=32))
# RIGHT: cryptographically secure
token = secrets.token_urlsafe(32) # URL-safe API token
hex_key = secrets.token_hex(16) # 128-bit key material
# constant-time comparison defeats timing attacks on secret equality
if secrets.compare_digest(provided, expected):
grant_access()
Python footgun -> safe replacement
deserialization
pickle.loads(untrusted)
use json.loads
yaml.load(...)
use yaml.safe_load
input & paths
trust the input
validate at the boundary (pydantic)
base / user_path
resolve + is_relative_to
randomness & crypto
random.choices for tokens
secrets.token_urlsafe
a == b on secrets
secrets.compare_digest
Each left cell is a real vulnerability class; each right cell is the standard-library one-liner that removes it.
A subprocess with shell=True and any input is the top Python finding
Static analysers (bandit) flag shell=True, yaml.load, pickle on untrusted data, and assert-based checks for good reason — they are the vulnerabilities that actually get exploited. Run bandit in CI and treat high-severity findings as blockers. The safe alternative to each is a single line away, so there is no reason to ship the dangerous form.