CWE-502: Deserialization of Untrusted Data
Overview
Insecure deserialization occurs when an application rebuilds objects from data it does not control. Native serialization formats - Java ObjectInputStream, Python pickle, PHP serialize(), and .NET BinaryFormatter - take the class to instantiate from the serialized bytes themselves, so whoever supplies those bytes decides which classes get built, and that is usually enough to run code inside the process.
Relationship to Other CWEs
- CWE-502 (this page) - Unsafe object instantiation via deserialization.
- CWE-94 (Code Injection) / CWE-95 (Eval Injection) - Unsafe code execution via dynamic evaluation.
- CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) - A peer, and the easier of the two to file this against by mistake, because both end with attacker-influenced object state. The test is what the attacker controls: which fields of an existing, expected type get set is CWE-915 (mass assignment, auto-binding); what type is constructed, or code reached during the reconstruction itself, is CWE-502. MITRE's own note on the pair says the boundary needs further exploration, so expect scanners to disagree with each other here.
- CWE-1321 (Prototype Pollution) - The JavaScript weakness that survives the fix on this page. Replacing
eval-based deserialization withJSON.parseremoves the code execution and leaves the payload intact as data - a subsequent deep merge or path write can still reachObject.prototype. SeeRelated Risk: Prototype Pollution via Unsafe Mergeon the JavaScript page.
OWASP Classification
A08:2025 - Software or Data Integrity Failures
Risk
Critical: Deserializing an attacker-controlled payload usually means remote code execution, and with it the process and everything it can reach. Authentication bypass, privilege escalation and denial of service are lesser outcomes of the same primitive. Java (gadget chains), Python (pickle) and .NET Framework are the most severe environments. On current .NET the picture is narrower and is set out below: BinaryFormatter has been removed from the platform, while JSON.NET's TypeNameHandling remains a live sink wherever it is enabled.
Remediation Steps
Core Principle: Never deserialize untrusted data into instantiable objects. Where that boundary cannot be removed, check integrity and type before anything is constructed.
Locate the insecure deserialization vulnerability
- Find the file, line and deserialization call named in the finding
- Trace where the serialized data comes from: user input, external files, databases, network requests
- Map the data flow from that source to the deserialization call
- Identify the serialization format: pickle, Java serialization, .NET BinaryFormatter, PHP serialize
- Check whether the data passes any validation or integrity check on the way
- Decide whether an attacker can control that source
Eliminate native deserialization of untrusted data (Primary Defense - BEST)
- Replace native serialization with JSON, XML, or Protocol Buffers
- Parse with a standard parser -
json.loads(),JSON.parse(),JsonSerializer- and no custom deserializers - Redesign the interface to pass structured data rather than serialized objects
- For config files, use JSON, or YAML loaded with
yaml.safe_load() - Remove
pickle,ObjectInputStream,BinaryFormatterandunserialize()from any path carrying untrusted data - This removes the deserialization attack surface rather than constraining it, which is why everything below it is a fallback
- The C#, Java, PHP and Python pages each carry a library safety matrix, mapping a library name from a finding to whether it can be configured safely at all
Use integrity checks if deserialization cannot be avoided
- Sign serialized data with an HMAC or a digital signature before it is stored or transmitted
- Keep signing keys in a key management system or secrets manager. An environment variable is a way for the platform to inject the key at process start, not a place to store it - see CWE-526
- Verify the signature before deserializing, and reject data whose signature is missing, invalid, or mismatched
- Use authenticated encryption, in the encrypt-then-MAC arrangement
- Rotate signing keys periodically, and version them so rotation stays graceful
- A signature stops a tampered payload, and is worth no more than the confidentiality of the signing key
Add class allowlist filters (Defense in Depth)
- Permit only specific, known-safe classes
- Use the framework's own filter: Java
ObjectInputFilter(JEP 290, built in since Java 9 - no third-party library needed), JSON.NETISerializationBinder, KryosetRegistrationRequired(true) - Reject any class that is not on the list
- In Python,
pickle.Unpickler.find_class()can allowlist, but treat it as a way to read legacy pickle from a source you control, not as a way to make pickle acceptable for external input - see the Python page - An allowlist limits gadget chain exploitation to whatever chains the listed classes can still build
- Test the allowlist in both directions. An allowlist is the control most likely to be shipped rejecting everything, because "the attack payload was refused" and "nothing works" look identical to a re-scan. Deserialize a legitimate instance of every allowlisted type before closing the finding
Apply runtime protections and monitoring
- Run application processes with least privilege (minimal OS permissions)
- Isolate deserialization in a container or sandbox
- Disable features that supply gadget chains and are not needed, such as Java RMI and JMX
- Segment the network to limit SSRF impact
- Log every deserialization with its data source and the class types involved
- Alert on unusual class loading, deserialization errors, and attempts to deserialize classes that were not expected
Test and verify deserialization security
- Replay the exact input from the finding; it should be rejected or handled without instantiating anything
- Send a known gadget chain payload for your framework - ysoserial for Java, a crafted pickle for Python - and confirm it fails
- Tamper with a signed payload and confirm HMAC verification rejects it
- Attempt to deserialize a class that is not on the allowlist; it should fail
- Exercise the JSON path end to end, so every feature that used the old format still works
- Confirm the business logic behaves the same after remediation
- Re-scan, and check both that the finding is gone and that the change introduced no new ones
Common Vulnerable Patterns
- Java:
ObjectInputStream.readObject()with untrusted data - Python:
pickle.load()orpickle.loads()with external data - .NET:
BinaryFormatter.Deserialize()with user-controlled input, andTypeNameHandlingother thanNonein JSON.NET - PHP:
unserialize()with data from requests/cookies - Ruby:
Marshal.load()with untrusted sources - Go:
gob.Decoder.Decode()with untrusted data, and any decoder writing straight onto a struct that carries privileged fields
Not every sink on that list is still reachable, and the version matters when
you triage one. .NET is the case where the platform closed it: BinaryFormatter
was made obsolete in .NET 5, its obsoletion became a build error and its
implementation was removed in .NET 9, and on .NET 10 a suppressed call throws
PlatformNotSupportedException: BinaryFormatter serialization and
deserialization have been removed. A finding against BinaryFormatter on a
current target framework is a migration task rather than a live exploit path -
but the same code on .NET Framework 4.8, which is still supported, is fully
exploitable, so establish which runtime the finding is on before deciding.
Nothing equivalent has happened to pickle, unserialize(), Marshal.load()
or ObjectInputStream; all four behave today as described here.
Untrusted Deserialization Leading to RCE
// VULNERABLE - pseudo-code
data = request.param('session')
obj = native_deserialize(data) // DANGEROUS - can instantiate arbitrary classes
// Attack: attacker crafts a malicious serialized payload using a known gadget chain
// Result: arbitrary code execution on the server
Why this is vulnerable: A native serialization format (Java ObjectInputStream, Python pickle, .NET BinaryFormatter, PHP unserialize()) lets the payload decide which classes get built. An attacker chains the side effects of those constructions - a "gadget chain" - into remote code execution or authentication bypass. The code runs during deserialization itself, not when the resulting object is later used, so inspecting the object afterwards is already too late.
Secure Patterns
Safe Structured-Data Deserialization With Validation
// BEFORE (unsafe) - native format, no validation
obj = native_deserialize(data)
// AFTER (safe) - a safe format that only produces primitive types, plus explicit validation
obj = json_deserialize(data) // SECURE - creates strings/numbers/arrays/objects, never arbitrary classes
if not is_object(obj):
raise ValidationError('invalid data format')
required_fields = ['username', 'email']
if not all_present(obj, required_fields):
raise ValidationError('missing required fields')
user = create_user(obj.username, obj.email) // validated structure, safe to use
Why this works:
- JSON only produces primitive types - strings, numbers, arrays, objects - so there is no arbitrary class for a gadget chain to reach
- Validating the parsed structure afterwards means only the expected shape reaches application code
For more examples: See the Language-Specific Guidance section below for secure deserialization patterns per ecosystem, including HMAC verification and alternative formats.
Common Pitfalls
- Switching formats without closing the actual data-flow risk: Moving from native serialization to JSON stops the object-instantiation attack, the gadget chains. It does nothing about the payload's contents. If the parsed data is a loosely-typed map that later builds a SQL query, file path, or shell command from attacker-supplied keys, the deserialization step is safe and the injection downstream of it is untouched.
- Leaving polymorphic type resolution enabled after migrating to JSON: Replacing native serialization with a JSON library only fixes the weakness if arbitrary-class instantiation is disabled too. A library that still honors a
type/$type/__class__field in the payload to decide which class to instantiate reproduces the same arbitrary-object-creation weakness through a different library, under a different field name. - Writing an allowlist broad enough to still contain a gadget: A package-prefix allowlist (
com.company.*), or one that includes common framework, logging, and collection classes for convenience, feels safe because it excludes exotic types. But most real gadget chains are built from exactly those ubiquitous utility and collection classes, so a convenience-driven allowlist can still admit an exploitable chain. - Trusting a signature without controlling who can produce one: A valid HMAC or signature check stops a tampered payload. It means as much as the key's own confidentiality and no more: if the signing key is shared across environments, embedded in a distributed client, or reachable by a lower-trust service, anyone who can read it can forge arbitrary "validly signed" malicious objects.
Migration Considerations
Changing serialization format invalidates everything already serialized in the old one: sessions, cache, message queues, stored objects.
What Breaks
- Every active session is invalidated, so users are logged out when you switch from pickle to JSON
- Redis and Memcached entries written in the old format read as corrupt
- Celery and RabbitMQ tasks serialized with pickle cannot be processed
- Database BLOBs holding serialized objects no longer deserialize
- API contracts break where you exchange serialized data with partners
- Historical serialized data in logs becomes unreadable
Migration Approach
Dual-Read Strategy (Recommended for Sessions/Cache)
Support both old (pickle) and new (JSON) serialization formats:
- Store which serialization format was used alongside the data
-
Deserialize with both, in order:
- Try JSON first (new secure format)
- If JSON fails, try legacy format (pickle)
- Mark legacy data for upgrade
-
Write all new and updated data as JSON
- Upgrade on write: when legacy data is read and modified, re-serialize it as JSON
- Track what percentage of data is on the new format
Big-Bang Migration (Acceptable for Sessions)
Since sessions are short-lived, you can invalidate all sessions:
- Clear all session data (logs everyone out)
- Set short expiry (e.g., 24 hours) on old sessions
- Send email notification about security upgrade
- Users log in again and get new JSON-based sessions
Migration for Stored Data (Database BLOBs)
For long-lived serialized objects in database:
- Process records in chunks, so the migration does not overload the database
- Read the existing pickle-serialized data with the old format
- Convert complex objects to JSON-compatible types
- Write the record back as JSON
- Mark the record as migrated in its version metadata
- Log and skip records that cannot be converted
Rollback Procedures
For Session Changes:
- Deploy the previous version, which still supports pickle
- No data restore is needed - sessions regenerate on login
- Tell users they need to log in again
For Stored Data:
- Stop the batch migration script
- Restore the table from the pre-migration backup
- Deploy the previous application version
- Check which object types failed conversion
Testing Recommendations
Pre-Migration Testing:
- Test session serialization with JSON format
- Verify dual-read handles both pickle and JSON
- Test user workflow: login → action → logout
- Load test: JSON serialization performance
- Test complex session data (lists, nested dicts, custom objects)
- Verify pickle sessions still work during transition
Post-Migration Monitoring:
- Monitor session deserialization errors
- Track login/logout rates (should remain constant)
- Alert on serialization format distribution changes
- Monitor cache/Redis performance
Key Metrics:
- Total sessions/cached objects
- Objects using JSON format
- Objects using pickle format
- Migration percentage
- Deserialization error rate
Language-Specific Guidance
- C# - Migrate off BinaryFormatter, use System.Text.Json, keep JSON.NET
TypeNameHandlingatNone - Go - Avoid gob with untrusted data, use JSON with validation
- Java - Avoid native serialization, use JSON with validation
- JavaScript/Node.js - JSON.parse with validation, avoid eval
- PHP - Avoid unserialize, use JSON with type checking
- Python - Avoid pickle, use JSON with schema validation