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.
| Python | pickle, cPickle, dill, shelve, joblib.load, pandas.read_pickle, torch.load without weights_only=True, yaml.load without SafeLoader |
| Java | ObjectInputStream.readObject, XMLDecoder, XStream and Jackson with default typing enabled |
| Other | PHP unserialize, Ruby Marshal.load and YAML.load, .NET BinaryFormatter and LosFormatter, Node.js node-serialize |
| Safe replacements | json, yaml.safe_load, Protobuf, MessagePack, torch.load(weights_only=True), Jackson with default typing off |
| Typical impact | Remote code execution before any application logic runs |
| Not a fix | Scanning the byte stream for gadget names, or deserializing then validating the result |
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.
Vulnerable
import yaml
config = yaml.load(request.data) # full loader: !!python/object/apply:os.systemSecure
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.
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.
Each of these is a pull request Orbis AppSec opened against a real open-source repository.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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