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

yaml.load('a: 1')

TypeError: Loader is a required argument

yaml.load(payload, Loader=yaml.FullLoader)

ConstructorError — refused

yaml.unsafe_load('!!python/name:os.system')

returns <built-in function system>

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.

Limitations

Only the call site is examined. A reader reached through an alias assigned at run time, or a loader passed in as a variable, is not resolved.

© 2026 Xygeni. All rights reserved