Security Guides
Insecure deserialization: the risk of letting external data choose the object type, and how to prevent it
Turning bytes back into objects can, depending on the format, let external data decide which class gets built. OWASP says attacks on deserializers have allowed denial of service, access control bypass and remote code execution — and that .NET's BinaryFormatter cannot be secured.
Turning received bytes back into objects becomes dangerous not because the contents are malformed. In some formats, external data gets to decide which type of object is constructed.
Why input validation arrives too late
Pure data format (JSON / XML)
external data decides values only → you receive it, then map it into your own types → validation works
Object-restoring format
external data decides values and the type → code belonging to the chosen type runs during restoration → execution never reaches your checks
Most input validation asks whether the value is acceptable. But in an object-restoring format, the restoration itself — which class is instantiated, and what code runs along the way — is steered by external data. Something can finish executing before your validation is ever called.
It is the same shape as prototype pollution: external data deciding structure rather than values. Different name, different language, same instinct for the defense.
What goes wrong (the three OWASP lists)
- Denial of service
- Making the restoration itself expensive, or breaking it, to stop the application
- Access control bypass
- Objects representing permissions or roles constructed to the attacker's convenience
- Remote code execution
- Chaining the routines invoked during restoration to reach arbitrary code execution — the most serious outcome
"Our framework doesn't do that" is not a safe assumption
This is not only a story about older languages. When we counted Next.js vulnerabilities in the NVD data, deserialization (CWE-502) sat high in the CWE distribution — because modern frameworks ship paths that deserialise the request body, Server Actions among them (do Next.js and React have a lot of vulnerabilities?). "I did not write it" does not mean "it is not there".
Two defenses
1. Do not let anything choose types (first choice)
OWASP's wording: "By switching to a pure data format like JSON or XML, you lessen the chance of custom deserialization logic being repurposed towards malicious ends."
Receive external input in a format that carries values only, then map it into your own types in your own code. This alone stops the attacker from choosing what gets built. In most codebases it solves the problem.
2. Sign it, and refuse the unsigned
Where a custom format is unavoidable, the guidance notes you "could sign them as part of the serialization process" and then "choose not to deserialize any message which didn't have an authenticated signature".
The point is to verify before restoring. Restoring first and inspecting afterwards can already be too late, for the reason above. The order is the whole control.
Some mechanisms cannot be secured by configuration
On .NET's BinaryFormatter, OWASP states outright that "the BinaryFormatter type is dangerous and cannot be secured".
That sentence matters operationally: tools exist for which "it's fine if you use it correctly" is simply not true. Hardening options, adding validation, writing an allow-list — none of it makes it safe. The only control is not using it.
So when auditing, first separate what configuration can protect from what it cannot. Spending effort on the second category is not mitigation; it is postponement.
What OWASP names, by language
- PHP
- Avoid
unserialize(); prefer JSON - Python
pickle/c_pickle, PyYAML'sloadandjsonpickleare listed as dangerous- Java
- Override
ObjectInputStream#resolveClass()to restrict which classes can be restored - .NET
BinaryFormatteris dangerous and cannot be secured — do not use it
The common thread: a language's convenient built-in persistence machinery is precisely what must never face external data. It was built to move whole objects between parties that trust each other, and it does not assume a hostile sender.
Safe and unsafe APIs by language
The table lines up what each language's official documentation warns you not to use on external data, and what it recommends instead. If something from the left column appears in your code, find out where its data comes from.
| Language | Do not use on external data | Use instead | What the official docs say |
|---|---|---|---|
| Python | pickle.load / pickle.loads, marshal, yaml.load (with Loader=yaml.Loader) and yaml.unsafe_load, jsonpickle.decode | json.loads, yaml.safe_load | pickle "is not secure"; sign with hmac if you need tamper detection |
| PHP | unserialize() (even with allowed_classes set) | json_decode() / json_encode() | Do not pass untrusted input regardless of allowed_classes |
| Ruby | Marshal.load | JSON, or another format that only loads basic types | Loading from an untrusted source can lead to remote code execution |
| Java | ObjectInputStream#readObject, XMLDecoder, XStream#fromXML on external data | Map JSON into your own classes (DTOs) | If unavoidable, limit allowed classes with ObjectInputFilter |
| .NET | BinaryFormatter, SoapFormatter, NetDataContractSerializer, LosFormatter, ObjectStateFormatter, Json.NET TypeNameHandling other than None | System.Text.Json, XmlSerializer, DataContractSerializer | From .NET 9, BinaryFormatter throws when used |
Put side by side, the difference looks like this. The unsafe version turns whatever arrived straight back into an object; the safe version reads it as values and moves only the fields you need into your own type.
# Avoid: turning the received bytes straight back into an object
order = pickle.loads(request_body)
# Use: read values, then move only the fields you need into your own type
data = json.loads(request_body)
order = Order(id=int(data["id"]), quantity=int(data["quantity"]))In the safe version, your code (Order) decides which class gets built. The incoming data only decides the values of id and quantity, and ordinary input validation works on those.
What to check in your own code
List every place external data first arrives
Request bodies, cookies, sessions, queues, uploads, webhooks, caches — every point where something is stored and later restored. Sessions and caches are the easy misses: data you think of as your own can be reachable from outside depending on the path.
Find which of those use a type-restoring format
Search for the functions and classes named in the list above. Those are the first things to remove. If a mechanism that cannot be secured (BinaryFormatter and similar) is in use, that is the top priority — nothing but migration resolves it.
Move to a pure data format
Receive JSON or XML and map it into your own types in your own code. Apply schema validation during that mapping and you close unexpected keys at the same time (the same remedy as for prototype pollution).
Where you cannot move, protect it with a signature
If the format cannot change, sign at serialization time and verify before restoring. Make "no signature, no restore" the default. Handle the key as in what is dangerous about .env and API keys — never in the same place as the code.
Count the restoration your dependencies do
Even where you wrote none, a dependency may be restoring objects. CVEs classified as CWE-502 keep appearing, so put them under machine monitoring (getting started with osv-scanner / a practical CVE remediation playbook).
How to check your own environment
This turns the "find it" part of the steps above into concrete commands and settings. Every check only reads your own repository and your own servers.
Text-search the source code
Start by listing every candidate. With ripgrep (rg) the searches look like this (grep -rnE takes the same patterns).
# Python
rg -n "pickle\.loads?\(|marshal\.loads?\(|yaml\.load\(|yaml\.unsafe_load|jsonpickle\.decode"
# PHP
rg -n "unserialize\("
# Ruby
rg -n "Marshal\.load"
# Java
rg -n "ObjectInputStream|readObject\(|XMLDecoder|fromXML\("
# .NET
rg -n "BinaryFormatter|SoapFormatter|NetDataContractSerializer|LosFormatter|ObjectStateFormatter|TypeNameHandling"Not every hit is dangerous. For each one, write down where the data it receives comes from, and put first anything connected to a path outsiders can reach: requests, cookies, uploads, shared storage.
Look for suppressed warnings
In .NET, using BinaryFormatter produces the SYSLIB0011 warning or error. Configuration that suppresses it marks a place where a dangerous call is still in the code.
rg -n "SYSLIB0011|CA23[0-3][0-9]" --glob "*.cs" --glob "*.csproj" --glob ".editorconfig" --glob "*.props"If you find #pragma warning disable SYSLIB0011 or the ID inside <NoWarn>, confirm why it is there and when it will be migrated.
Turn on static-analysis rules
For Python, the relevant Bandit checks are B301 (pickle and similar), B302 (marshal) and B506 (yaml.load); bandit -r . -t B301,B302,B506 runs just those three. For .NET, the relevant code analysis rules are the CA2300 series (for example CA2326, which flags Json.NET's TypeNameHandling). CA2326 is not enabled by default even in .NET 10, so turn it on in .editorconfig with a line such as dotnet_diagnostic.CA2326.severity = warning. Run these in CI and new calls get stopped too.
For Java, check the runtime filter settings
Java can restrict, at run time, which classes deserialization may create (serialization filters). The mechanism arrived in JDK 9 (JEP 290) and became switchable per context in Java 17 (JEP 415). For a running process, look for jdk.serialFilter in the output of jcmd <PID> VM.system_properties. It can also be set as a security property in the java.security file, so check both. If it is in neither place and your code does not call ObjectInputFilter.Config.setSerialFilter, there is no process-wide filter.
Check dependencies for known vulnerabilities
Even with no such call in your own code, a dependency may restore objects. List known vulnerabilities in your dependencies with osv-scanner or a similar tool, and update the ones classified as CWE-502 (deserialization of untrusted data) first.
Common mistakes and how to fix them
| Mistake | Why it is a problem | Fix |
|---|---|---|
| Trusting data because "it is internal" or "I saved this file myself" | Microsoft gives examples of data crossing a trust boundary without the developer noticing: shared save files, cloud sync, a compromised employee machine | If any path from outside can reach it, treat it as external data. Do not restore anything unsigned |
| Adding dangerous classes to a block list one by one | It does nothing against combinations not on the list or not yet known. PHP's manual says not to pass untrusted input even with allowed_classes | Change the format to JSON or similar. Only where unavoidable, list the classes you allow |
| Restoring first, then validating the contents | Code runs during restoration, so validation comes too late | Put signature verification before restoration |
| Suppressing the warning and calling it fixed | Silencing SYSLIB0011 or CA2326 leaves the dangerous call in place | Search for suppressions and set a migration deadline |
| Assuming "it is JSON, so it is safe" | Settings that let JSON name its own types, such as Json.NET's TypeNameHandling, turn it into a type-choosing format | Keep TypeNameHandling at None (the default) |
Using yaml.load with PyYAML | PyYAML's documentation says yaml.load is as powerful as pickle and may call any Python function | Replace it with yaml.safe_load |
A real example: restoration inside an extension
This site's bulletin on CVE-2026-45247 covers a PHP object injection (CWE-502) in the Magento 2 extension "Mirasvit Full Page Cache Warmer" before 1.11.12. It allows remote code execution without authentication, has a CVSS score of 9.3, and was added to CISA's catalog of known exploited vulnerabilities (KEV).
The lesson is that the restoration code was not written by the site operators; it was inside an extension they installed. That is why the "check dependencies" step above matters. The fix was updating the extension, and searching your own code would not have found it.
This site's view: ask what the input is allowed to decide, not whether the value is valid
Say "input validation" and most people picture checking the value — length, format, range. The threat in this article resolves before that point. Our framing: the real question about an input is not "is this value correct" but "how much have I let this input decide".
Only the value? The structure too (prototype pollution)? The type as well (this article)? Even where it lands (file upload vulnerabilities)? Count the decisions you have handed outside, and the dangerous places surface on their own.
Sources (primary)
- OWASP Cheat Sheet Series, "Deserialization Cheat Sheet" — cheatsheetseries.owasp.org (the three impacts, switching to a pure data format, signing for integrity, the language-specific notes and the statement about
BinaryFormatterall come from this document; checked 5 September 2026) - Python documentation, "pickle" — docs.python.org (the "not secure" warning; signing with
hmacand preferring JSON) - PyYAML Documentation — pyyaml.org (the difference between
yaml.loadandyaml.safe_load) - PHP Manual, "unserialize" — php.net (do not pass untrusted input regardless of
allowed_classes; use JSON) - Ruby documentation, "Marshal" — docs.ruby-lang.org
- Microsoft Learn, "Deserialization risks in use of BinaryFormatter and related types" — learn.microsoft.com (equally dangerous types, preferred alternatives, behavior from .NET 9, examples of crossing a trust boundary)
- Microsoft Learn, "SYSLIB0011" and "CA2326" — SYSLIB0011 / CA2326
- OpenJDK, "JEP 290: Filter Incoming Serialization Data" and "JEP 415: Context-Specific Deserialization Filters" — JEP 290 / JEP 415
- Bandit documentation (B301, B302, B506) — bandit.readthedocs.io
- The language table, the search steps and the common mistakes were checked against the documents above on 6 October 2026
Read next
- The same shape: prototype pollution in Node.js (external data deciding structure) / file upload vulnerabilities (external data deciding where things land)
- Glossary: what remote code execution (RCE) is
- Real example: CVE-2026-45247 (PHP object injection in a Magento extension)
- By framework: ASP.NET Core / Spring Boot / Django security
- Dependencies: getting started with osv-scanner / a practical CVE remediation playbook
FAQ
QWhy is deserialization so dangerous? Isn't it just data conversion?
Because in some formats it is not data conversion but object reconstruction. When the information about which class to instantiate travels inside the data, an attacker gets to choose what you build. OWASP states that attacks against deserializers have been found to allow denial-of-service, access control, or remote code execution attacks. What makes it hard to defend is that the attack succeeds before your value checks ever run.
QWhat is the most reliable defense?
Restrict externally sourced data to a format that cannot choose types. OWASP puts it this way: by switching to a pure data format like JSON or XML, you lessen the chance of custom deserialization logic being repurposed towards malicious ends. If a custom format is unavoidable, the same guidance notes you could sign messages as part of the serialization process and then choose not to deserialize any message without an authenticated signature. Change the format first; bind it with a signature if you cannot.
QCan I make it safe by hardening the configuration?
It depends on the mechanism. OWASP states plainly that .NET's BinaryFormatter type is dangerous and cannot be secured. Some machinery does not become safe through settings or extra validation — the only resolution is to stop using it. Separate what configuration can protect from what it cannot. Tools for which "it's fine if you use it correctly" does not hold genuinely exist.
QWhat should I watch for in my language?
Among what OWASP names directly: in PHP, avoid unserialize() and prefer JSON; in Python, pickle / c_pickle, PyYAML's load and jsonpickle are dangerous; in Java, override ObjectInputStream#resolveClass() to restrict which classes can be restored; in .NET, do not use BinaryFormatter. The common thread is that a language's convenient persistence machinery is exactly what you must not point at external data.
QHow do I find unsafe deserialization in my own code?
In three passes. First, text-search with ripgrep or grep for calls such as pickle.loads, unserialize, Marshal.load, ObjectInputStream and BinaryFormatter. Second, turn on static-analysis rules (Bandit's B301, B302 and B506 for Python; the CA2300-series code analysis rules for .NET). Third, scan your dependencies for known vulnerabilities with a tool such as osv-scanner and update the CWE-502 ones first. For every call you find, write down where the data it receives comes from, and fix the ones reachable from outside first.
QIs it always safe if I use JSON?
Not if you enable a setting that lets the data name its own types. The usual example is TypeNameHandling in Json.NET for .NET; Microsoft's code analysis rule CA2326 says not to use any value other than None. The benefit of JSON is that it carries values only, so the receiving code decides which class gets built. Check that you have not turned on a setting that removes that benefit.