Deserialization with a reader that reconstructs arbitrary objects
ID |
python.insecure_deserialization |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Misconfiguration |
Language |
Python |
Tags |
ASVS50:v1.5.2, CWE:502, NIST.SP.800-53, OWASP:2025:A08, PCI-DSS:6.5.1 |
Description
A payload is read with a deserializer that reconstructs whatever object the payload describes. The
payload then decides which classes are instantiated and what runs while they are built — pickle calls reduce on the way in, so a crafted payload is arbitrary code execution, not merely
unexpected data.
Rationale
python.code_injection_deserialization reports deserialization when a data path can be followed from an
external input to the read. That needs the input to be visible: a pickle.loads() in a codec or a helper
module, where the caller lives elsewhere, has no source to follow and is reported by nothing. Confirmed
on a full NetBox scan, where exactly that shape went unreported.
Its severity is low because the finding says nothing about whether an attacker-controlled payload
actually reaches the call — when that can be shown, python.code_injection_deserialization reports it
as critical, and this finding is dropped as a duplicate.
What is reported
pickle.load / loads, cPickle, dill, marshal.load / loads, jsonpickle.decode,
shelve.open, yaml.unsafe_load / unsafe_load_all, and yaml.load given Loader=yaml.Loader or
Loader=yaml.UnsafeLoader.
Matched on the resolved qualified name, so an aliased import is still recognized. Note that pickle is
not a sink in the shipped library metadata at all, which is tracked separately.
Not covered: a bare yaml.load
yaml.load(x) with no Loader is deliberately absent, and this is the distinction the whole sink list
is built on: a reader that is unsafe by contract is reported, a library default that a release might
have changed is not.
Measured on PyYAML 6.0.1:
| Call | Result |
|---|---|
|
|
|
|
|
returns |
FullLoader refuses the object-construction tags, so only the explicitly unsafe loaders reconstruct
arbitrary objects on a current PyYAML.
A bare yaml.load is left to python.insecure_parser, which already reports it, rather than duplicated
here. It is version-conditional rather than safe: PyYAML below 5.1 defaulted to the unsafe loader
(CVE-2017-18342), 5.1 to 5.4 fall back to FullLoader, and 6.0 makes the argument required. With the
resolved version invisible to the analysis the standing rule is recall over precision, so it stays
reported there; narrowing it by version is tracked separately.
There is no restriction to look for
The documented way to limit what a pickle may reconstruct is an Unpickler subclass overriding
find_class. That restricts the subclass’s own .load(), which does not resolve to a name this rule
matches, so such code is out of scope rather than suppressed.
A module-level pickle.loads is always reported, because no find_class override anywhere can restrict
it. A first draft of this rule suppressed a whole file when it saw a find_class definition, which
silenced real findings and protected nothing.
Examples
Reported:
def restore(blob: str):
return pickle.loads(base64.b64decode(blob)) # reported
Not reported — a format that names no classes:
def restore(text: str):
return json.loads(text)
Remediation
Carry the data in a format that names no classes. JSON is the usual answer, and for a fixed shape a
schema library gives the same convenience pickle was reached for:
def restore(text: str) -> Order:
return Order(**json.loads(text))
For YAML, use yaml.safe_load or pass Loader=yaml.SafeLoader. Where pickle cannot be removed,
restrict it with an Unpickler subclass that allowlists in find_class, and treat the allowlist as
security-relevant code.