Insecure deserialization: pickle, Java serialization and YAML

Formats that can reconstruct arbitrary objects — Python `pickle`, Java `ObjectInputStream`, Ruby `Marshal`, PHP `unserialize`, and `yaml.load` with the default loader — invoke constructors and magic methods while parsing, so the byte stream *is* code. There is no way to validate a pickle before loading it, because the validation would have to run the pickle. The fix is to change format: JSON (or Protobuf/MessagePack) with an explicit schema, mapped field-by-field into your own types. Where the format cannot change, authenticate the payload with an HMAC you verify before parsing, and constrain the classes that may be instantiated with a strict allowlist filter.

At a glance

Pythonpickle, cPickle, dill, shelve, joblib.load, pandas.read_pickle, torch.load without weights_only=True, yaml.load without SafeLoader
JavaObjectInputStream.readObject, XMLDecoder, XStream and Jackson with default typing enabled
OtherPHP unserialize, Ruby Marshal.load and YAML.load, .NET BinaryFormatter and LosFormatter, Node.js node-serialize
Safe replacementsjson, yaml.safe_load, Protobuf, MessagePack, torch.load(weights_only=True), Jackson with default typing off
Typical impactRemote code execution before any application logic runs
Not a fixScanning the byte stream for gadget names, or deserializing then validating the result

Vulnerable and fixed, side by side

Python — pickle

Vulnerable

import base64, pickle

def load_session(cookie: str):
    return pickle.loads(base64.b64decode(cookie))

# A crafted payload's __reduce__ runs os.system during loads(); nothing
# in the application ever sees the object.

Secure

import base64, hmac, hashlib, json, os

KEY = os.environb[b"SESSION_KEY"]

def load_session(cookie: str) -> dict:
    raw = base64.b64decode(cookie)
    body, tag = raw[:-32], raw[-32:]
    expected = hmac.new(KEY, body, hashlib.sha256).digest()
    if not hmac.compare_digest(tag, expected):
        raise ValueError("bad session signature")

    data = json.loads(body)              # JSON cannot construct objects
    return {
        "user_id": int(data["user_id"]),  # map fields explicitly
        "roles": [str(r) for r in data.get("roles", [])],
    }

Two independent changes: the format no longer executes anything, and the payload is authenticated so a forged one is rejected before parsing. The HMAC alone would not be enough if the key ever leaked — the format change is what removes the class.

Python — YAML

Vulnerable

import yaml

config = yaml.load(request.data)   # full loader: !!python/object/apply:os.system

Secure

import yaml

config = yaml.safe_load(request.data)
if not isinstance(config, dict):
    raise ValueError("expected a mapping")

`yaml.load` without an explicit `Loader` has warned since PyYAML 5.1 and defaults to the full loader in older versions. `safe_load` restricts construction to plain scalars, lists and dicts — which is what a config file needs anyway.

Java

Vulnerable

try (ObjectInputStream ois = new ObjectInputStream(request.getInputStream())) {
    Order order = (Order) ois.readObject();   // gadget chain runs before the cast
}

Secure

// Preferred: change the format.
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// Default typing must stay OFF — enabling it reintroduces the gadget problem in JSON.
Order order = mapper.readValue(request.getInputStream(), Order.class);

// If the wire format is fixed, filter the classes (Java 9+ / 8u121+):
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.example.dto.*;java.lang.String;java.util.ArrayList;!*");
try (ObjectInputStream ois = new ObjectInputStream(in)) {
    ois.setObjectInputFilter(filter);
    Order order = (Order) ois.readObject();
}

The cast to `Order` happens after the whole graph is reconstructed, so the exploit has already run by the time a ClassCastException would fire. The filter's trailing `!*` — deny everything not listed — is the part that makes it an allowlist.

How to find it in your codebase

  • Grep the sinks: `rg -n 'pickle\.loads?|yaml\.load\(|Marshal\.load|unserialize\(|ObjectInputStream|BinaryFormatter|node-serialize'`.
  • Bandit B301 (pickle), B506 (yaml_load); Semgrep `python.lang.security.deserialization.pickle`, `java.lang.security.audit.object-deserialization`.
  • Check `torch.load` / `joblib.load` call sites specifically — model files downloaded from a hub are untrusted input, and `torch.load` used pickle by default until `weights_only=True` became the default in 2.6.
  • For Jackson, assert in a test that default typing is disabled: `enableDefaultTyping` and `activateDefaultTyping` are the two calls that reopen the hole.
  • Audit anywhere a serialized blob crosses a trust boundary: cookies, caches, message queues, and files uploaded by users.

Fix checklist

  1. Change the format to JSON, Protobuf or MessagePack and map fields into your own types explicitly.
  2. Replace `yaml.load` with `yaml.safe_load` and assert the parsed shape.
  3. Where the format is fixed by a protocol you do not own, verify an HMAC before parsing and install a deny-by-default class filter.
  4. Set `weights_only=True` on `torch.load` and pin model checksums.
  5. Validate the mapped object after parsing — bounds, enums, ownership. Deserialization safety is not authorisation.
  6. Add a test that feeds a known gadget payload and asserts the request is rejected rather than executed.

Fixes we shipped

Each of these is a pull request Orbis AppSec opened against a real open-source repository.

How Arbitrary Code Execution Happens in protobufjs and How to Fix It

CVE-2026-41242 is a critical vulnerability in protobufjs versions 8.0.0 and earlier that allows attackers to execute arbitrary code by injecting malicious type fields into protobuf definitions. The fix upgrades the dependency from `^8.0.0` to `^8.6.6` in `core/package.json`, eliminating the unsafe code path that processed attacker-controlled type metadata without validation.

How Quadratic CPU Consumption Happens in JS-YAML and How to Fix It

A critical vulnerability in JS-YAML versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through maliciously crafted YAML input using the `!!omap` tag resolver. The vulnerability stems from inefficient array operations in the ordered map resolution logic, which could be exploited for denial-of-service attacks. Upgrading to JS-YAML 4.3.1 or 3.15.1 patches this attack surface by optimizing the computational complexity of ordered map processing.

How Type Confusion Vulnerabilities Happen in JavaScript Dependencies and How to Fix Them

A critical type confusion vulnerability (CVE-2021-23436) was discovered in immer 9.0.7, a popular immutable state management library used in the client application. By upgrading to immer 9.0.6, the vulnerability was patched, eliminating a flaw that could have allowed attackers to bypass previous security fixes (CVE-2020-28477). This fix demonstrates why keeping dependencies current is essential for maintaining application security.

How Prototype Pollution Happens in i18next-fs-backend and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-48713) was discovered in i18next-fs-backend versions prior to 2.6.6, where specially crafted missing-key strings could pollute the JavaScript object prototype. This fix upgrades the dependency to patch the vulnerability and prevent attackers from injecting malicious properties into application objects.

How Quadratic CPU Consumption in js-yaml's !!omap Resolution Happens in Node.js and How to Fix It

A high-severity algorithmic complexity vulnerability (GHSA-5p4m-2wfm-xmqj) in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through crafted YAML input using the `!!omap` tag. The fix upgrades js-yaml from 4.1.1 to 4.3.1 in the Audex desktop music player, eliminating a denial-of-service vector that could freeze the Electron application when parsing untrusted YAML content.

How Prototype Pollution Happens in JavaScript Carousel Libraries and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-27212) was discovered in Swiper versions up to 11.2.10, a popular JavaScript carousel library used in production web applications. This vulnerability could allow attackers to manipulate application behavior through the prototype chain. The fix involved upgrading Swiper from 11.2.10 to 12.1.2, which patches the underlying prototype pollution flaw.

How Quadratic CPU Consumption in YAML Parsing Happens in Node.js and How to Fix It

A critical vulnerability in js-yaml's `!!omap` tag resolution allowed attackers to craft malicious YAML files that consumed CPU resources quadratically, leading to denial of service. The Orbis AppSec team identified this unpatched vulnerability in the docs-site project and automatically upgraded js-yaml to versions 4.3.1 and 3.15.1, which include CVE-2026-59870 backports that fix the algorithmic complexity issue.

How Information Disclosure and Denial of Service Vulnerabilities Happen in PostCSS and How to Fix Them

PostCSS 8.5.6 contained a critical vulnerability that could enable attackers to cause denial of service and information disclosure through specially crafted CSS input. This blog post explores how the vulnerability manifested in the dependency tree and how upgrading to PostCSS 8.5.23 eliminates the attack surface.

Browse every deserialization case study

Frequently asked questions

Can a pickle be validated before loading it?

No. Pickle is a small stack machine, and the opcodes that make it dangerous (`REDUCE`, `GLOBAL`) are executed by the parser as it goes. Anything that inspected the stream thoroughly enough to be sure would be re-implementing the interpreter. `pickle.Unpickler` with an overridden `find_class` can restrict the classes reachable, which is a real mitigation for a trusted-but-versioned stream, but it is not a defence against attacker-supplied bytes.

Is signing the payload enough to keep using pickle?

It stops forgery, which is most of the practical risk, and it is the right stopgap when you cannot change the format today. It is not equivalent to fixing the bug: the signing key becomes a code-execution key, so any key leak, any timing-unsafe comparison, and any environment where the key is shared with a lower-trust component turns back into RCE.

Is JSON always safe to deserialize?

The format itself constructs only strings, numbers, booleans, arrays and objects, so there is no gadget surface — but libraries can add one back. Jackson's default typing, `pickle`-backed "JSON" wrappers, and any framework that instantiates classes named in the payload reintroduce the problem. Keep polymorphic type handling off, and map fields explicitly rather than binding straight onto a domain object.

Are machine-learning model files affected?

Yes — a `.pt`, `.pkl` or `.joblib` file is a serialized object graph, so loading one from an untrusted source is the same bug with a friendlier extension. Use safetensors where possible, `torch.load(..., weights_only=True)` otherwise, and verify a checksum you obtained separately from the file.

Let Orbis AppSec find these for you

Orbis AppSec scans your GitHub repositories, traces the taint from source to sink, and opens a pull request with the fix applied and verified.

Try Orbis AppSec

Authoritative sources