Last reviewed: 2026-10-20

Direct answer

Prevent insecure deserialization in a coding-agent patch by treating every object reconstruction step as a trust-boundary operation. First ask whether the feature needs to restore executable or polymorphic objects at all. Prefer a plain data format such as JSON, parse it into a deliberately narrow data-transfer shape, reject unknown fields and oversized inputs, and construct application objects explicitly. For Python pickle from an untrusted or tamperable source, and for BinaryFormatter, replace the format. For another deliberately retained legacy protocol, authenticate its bytes before object reconstruction, configure supported type and resource restrictions before reading, and isolate the worker. These controls reduce risk; they do not establish that arbitrary native objects are safe to accept.

This ordering matters. OWASP describes attacks against native deserializers that can cause denial of service, access-control changes, or remote code execution. Python’s official documentation says that unpickling can execute arbitrary code and that pickle must never process data that came from an untrusted source or could have been tampered with. Microsoft’s BinaryFormatter guidance is stronger still: BinaryFormatter is dangerous, cannot be made secure by configuration, and should be removed even when an application believes its input is trusted. A patch that deserializes first and checks the resulting type afterward is already too late.

The review question is therefore not “does this payload look like our object?” It is “what code, types, memory, and side effects can the input select before the application has made an authorization decision?” Keep the answer concrete in the patch, tests, and operational evidence.

Who this is for

Use this guide when an agent changes a cache reader, job queue, session store, webhook consumer, import endpoint, plugin manifest, model-output adapter, background-worker protocol, or any code that calls a native serializer. Search for names such as pickle.loads, pickle.load, yaml.load, yaml.unsafe_load, ObjectInputStream.readObject, BinaryFormatter.Deserialize, TypeNameHandling, jsonpickle.decode, XMLDecoder, readResolve, and custom “hydrate” or “restore” helpers. Inspect wrappers and framework defaults as well as direct calls.

Repository files and internal queues are not automatically safe. A branch, uploaded artifact, support attachment, cloud-synced save file, database row, or message from another worker can cross a trust boundary after the original producer was compromised. Pair this review with the site’s prototype-pollution workflow when parsed data becomes a JavaScript object, and with the process-launch review when a reconstructed value can select a tool or operating-system action.

Key takeaways

  • Prefer a plain data interchange format and explicit field construction over native object graphs.
  • Treat type metadata, class names, constructors, callbacks, and property setters as input-controlled authority unless the contract proves otherwise.
  • Authenticate a message before deserialization when signed serialized data is genuinely required. Integrity checks do not make an unsafe format safe; they establish who approved the bytes.
  • Enforce byte, depth, reference, collection, and execution-time limits before and during parsing. A class allowlist alone does not bound resource consumption.
  • Validate the complete shape, types, ranges, and unknown-field policy before mapping data into domain objects.
  • Keep deserializers out of processes with network credentials, filesystem write access, or process-launch authority when the design permits isolation.
  • Test valid input, malformed input, gadget-like type markers, oversized graphs, duplicate fields, and side-effect attempts in disposable processes.
  • Record stable policy and failure codes. Do not log raw payloads, serialized bytes, secrets, or exception text that repeats attacker-controlled input.

Sources checked

Three primary public references were fetched live on 2026-10-20. The OWASP Deserialization Cheat Sheet identifies unsafe native formats and recommends safer data formats, signatures where appropriate, and Java filtering with class and resource limits. Python’s pickle documentation explicitly warns that unpickling can execute arbitrary code and recommends safer formats such as JSON for untrusted input. Microsoft’s BinaryFormatter security guide says BinaryFormatter is insecure and cannot be made safe by switches or binders, and lists safer alternatives.

These maintained references support an evergreen review, rather than a new vulnerability announcement. The contract, fixture matrix, logging choices, and rollout workflow below are engineering recommendations. They require repository-specific implementation and testing; no application test or production outcome is claimed.

Contract details to verify

Define the data contract before choosing a serializer

Write down the message purpose, producer, consumer, transport, maximum size, expected fields, versioning rule, and failure behavior. For a job request, a safe contract might contain an operation enum, an opaque job ID, a bounded list of input IDs, and a deadline. It does not contain a class name, module path, constructor arguments, callable, or arbitrary nested object. Unknown fields should be rejected unless compatibility requires an explicit ignore rule.

Separate the transfer record from the domain object. Parse into plain data, validate it, and then call a constructor or factory with named, trusted arguments. This makes authorization and defaulting visible. Do not deserialize directly into an object that owns file handles, network clients, credentials, callbacks, or lifecycle hooks. A type annotation or interface documents an expected shape; it does not enforce the shape at runtime.

Remove native object reconstruction where possible

JSON or another constrained data format reduces the amount of code that interprets input as behavior, but it is not an authorization mechanism. For a flat record, reject unexpected keys and nested values, excessive string lengths, and values outside the business range. For a nested record, define and validate every level. Configure the parser to reject duplicate keys before it collapses them into one value; ordinary schema validation afterward cannot recover that evidence. Use a schema validator with an explicit additional-property policy, then construct a fresh record. Do not pass a parsed object into a generic merge that can change prototypes, invoke setters, or overwrite trusted defaults.

For YAML, use a safe loader that creates data rather than arbitrary application objects, and do not register constructors that turn input into executable types. For Python, replace pickle for externally influenced data with JSON or a deliberately defined binary record. For .NET, migrate away from BinaryFormatter and its unrestricted polymorphic relatives. For Java, prefer a data-transfer format; if Java serialization remains, configure filtering before reading and define both allowed classes and resource limits.

Authenticate before parsing when a signed format is required

A signature or message authentication code can establish that a trusted producer approved the exact bytes. Verify the exact serialized bytes before invoking an object deserializer, using a key and algorithm selected by trusted configuration. Where the protocol carries version, purpose, audience, expiry, or replay metadata, authenticate that metadata too. A bounded plain-data envelope may be read to locate the signed bytes, but it must not instantiate payload-selected types. Do not accept an algorithm chosen by the payload, a key identifier that resolves to an arbitrary location, or a signature check that happens after object construction.

Integrity does not erase deserialization risk. A compromised signer, stolen key, replayed authorized message, or overly broad signed object can still produce dangerous behavior. Prefer signing a narrow data envelope with a version and purpose. Check the authenticated audience, expiry, and replay identity before constructing domain objects or performing the requested action. Under a protocol that requires authentication, missing or invalid authentication must stop object reconstruction. Plain JSON endpoints still need their own authentication and authorization rules.

Bound the graph and the runtime

A class allowlist can stop an unexpected type while a permitted type still consumes unbounded memory or CPU. Set a maximum encoded size before parsing, and enforce depth, reference count, collection length, string length, array size, and total work limits during parsing. OWASP recommends Java serialization filtering with class restrictions and limits on graph depth, references, array lengths, and stream bytes. It also warns that filters are not called for concretely encoded strings or primitives, so independently cap the input bytes. Apply transport and decompression limits too; a small compressed input can expand into a dangerous object graph.

Run legacy parsing in a dedicated low-privilege worker when feasible. Remove network access and deployment credentials, restrict filesystem writes, set a deadline, and terminate the whole worker on timeout. Isolation limits impact; it does not turn a dangerous parser into an approved input boundary. The worker should return a small validated data record, not a live object graph.

Review post-load behavior

Trace what happens after parsing. Constructors, readObject, readResolve, property setters, callbacks, validation hooks, and “on hydrate” events may perform I/O or mutate state. Search for implicit hooks in libraries and for code that immediately opens a file, sends a request, executes a command, or grants a permission based on a reconstructed field. Validate before those actions, and make the failure path side-effect free.

Keep the deserializer separate from code that has powerful gadgets or ambient authority. Microsoft’s guidance compares BinaryFormatter deserialization to launching an executable; OWASP similarly warns that native features can be repurposed for malicious effect. A patch that adds a parser to a web-facing process should receive a stricter review than one that reads a local, authenticated configuration under a dedicated identity.

Build a harmless adversarial test matrix

Use synthetic records and fresh child processes. The harness should capture whether deserialization was attempted, the stable rejection code, resource counters, and any downstream action. It must not execute exploit payloads or connect to real external systems.

FixtureRequired observation
Valid minimal recordExact schema, explicit defaults, and one approved domain object or data record
Unknown field or type markerRejected before object construction, with no state change
Malformed bytes and truncated inputControlled parse failure, no fallback to a permissive decoder
Oversized encoded inputRejected before allocation beyond the declared limit
Deep or cyclic graphBounded failure before stack or memory exhaustion
Excessive array, map, string, or reference countResource-limit failure with no downstream action
Python pickle or equivalent native payloadNever deserialized when the source is untrusted; migration path is exercised
Unsafe YAML constructor or polymorphic JSON markerSafe-loader or schema rejection before type creation
Valid signature with expired, wrong-audience, or replayed envelopeAuthenticated envelope rejected before domain-object construction or the requested action
Invalid signature over otherwise valid bytesNo deserializer call and no side effect
Duplicate or conflicting fieldsDeterministic reject or documented canonicalization, never last-write ambiguity
Post-load callback fixtureCallback is unavailable or cannot reach network, files, credentials, or process launch
Timeout during parsingWorker and descendants stop, temporary state is cleaned, and one terminal result is recorded

The test should assert the order of events. “The final object was rejected” is insufficient if a constructor or callback already ran. A recording adapter can check event order, but also exercise the actual boundary with inert fixtures; a mock cannot prove the real parser configuration. For a deliberately retained legacy path, use an isolated canary hook that increments only an in-memory counter. Check that a known allow case makes the harness observe it and a denied type leaves the counter at zero. Keep that fixture separate from tests proving that an untrusted pickle or BinaryFormatter path is never invoked.

Do not test by opening a real attacker URL, executing a gadget chain, or loading a developer’s cache. The purpose is to demonstrate the boundary and resource policy. Preserve fixture IDs, schema and filter revisions, parser version, byte and graph limits, and sanitized terminal results.

Happy-path operator workflow

  1. Inventory every deserialization call, wrapper, serializer default, and post-load hook touched by the patch.
  2. Write the message contract and identify which fields are data, which are authority, and which are never accepted from the sender.
  3. Replace native object graphs with plain data and explicit factories wherever the product allows it.
  4. If a signed legacy format is retained, verify its bytes before object reconstruction, and validate authenticated purpose, audience, freshness, and replay metadata before domain construction or action.
  5. Set byte, decompression, depth, reference, collection, string, CPU, and wall-clock limits before processing input.
  6. For a retained serializer that supports filtering, configure a narrow class or type policy before the first object is read. Reject unapproved polymorphic metadata. Do not use this step to justify retaining BinaryFormatter or untrusted pickle.
  7. Parse into a data-transfer record, validate every field and unknown-key rule, then construct the smallest domain object needed.
  8. Run the adversarial matrix in a disposable process and assert no constructor, callback, network, file, or process side effect occurred on rejection.
  9. Review the final diff for permissive fallback decoders, post-parse type checks, broad binders, inherited environment, and raw-payload logging.
  10. Retain the policy revision, parser/runtime versions, fixture digest, limit values, per-case result, and promotion decision.

Error-path operator workflow

Map malformed input, authentication failure, unknown type, schema mismatch, limit exhaustion, and timeout to stable codes such as deserialize_signature_denied, deserialize_type_denied, deserialize_schema_invalid, and deserialize_resource_limit. Return a controlled error without reflecting bytes, class names, paths, or exception messages into a response or broad log.

Discard partially built objects and staged files after any failure. Do not retry the same bytes through a permissive loader, downgrade from a verified parser to a native one, or increase limits automatically because a queue is delayed. If a legacy message cannot be authenticated or bounded, quarantine it for a restricted operator process and keep the production consumer closed.

If a worker times out, prove that its children and temporary resources are gone before admitting another message. If a signature fails, do not parse merely to produce a nicer diagnostic. If a schema evolves, deploy the new version and migration rule through review; do not accept arbitrary fields as a compatibility shortcut.

Failure modes

The patch checks the type after deserialization. Constructors and hooks may already have run. Enforce type and filter policy before reading the object.

The input is called internal, so the agent keeps native serialization. Internal queues, caches, and files can be poisoned by a compromised producer, account, branch, or sync service. Model every producer and storage layer explicitly.

A binder or allowlist is treated as a complete fix. Permitted types can still consume unbounded resources or trigger dangerous behavior. Add graph, byte, and runtime limits and isolate the parser.

A signature is checked after parsing. Move verification before deserialization. The parser must never see unauthenticated bytes.

JSON is declared safe without a schema. JSON avoids native object reconstruction but still permits oversized, ambiguous, or unauthorized data. Validate shape, fields, ranges, and downstream authority.

The agent catches an exception and retries with a different loader. A permissive fallback turns a controlled rejection into a vulnerability. Keep one reviewed parser policy.

Tests assert only a final error. A constructor, callback, or file write can happen before the error. Record event order and side-effect counters.

Logs include payloads or serialized bytes. They may contain credentials, personal data, executable instructions, or attack material. Record codes, sizes, versions, and hashes only when the retention policy permits them.

Isolation is treated as permission to keep unsafe input. A low-privilege worker reduces impact but still needs authentication, schema, and resource checks.

FAQ

Is a signed pickle or BinaryFormatter payload safe?

No. A valid signature proves that the signer approved the bytes; it does not make native reconstruction safe, limit resource use, or prevent a compromised signer from approving a dangerous object. Prefer a signed, narrow data envelope and parse it with a constrained format.

Is JSON always safe?

JSON does not itself create the arbitrary-code-execution behavior associated with Python pickle or unrestricted polymorphic object formats, but a JSON document can still be oversized, schema-invalid, or used to change authorization and operations. Validate it and construct explicit records.

Can a class allowlist make Java serialization safe?

It can reduce type risk when configured before reading, and Java filters can add resource limits. OWASP warns that a class check alone does not bound consumption and that filtering does not make arbitrary untrusted deserialization safe. Prefer a data format and keep the filter as a defense for a constrained legacy contract.

Should a parser run in a container?

Isolation is useful when legacy compatibility cannot be removed, especially when the worker has no network, secrets, or broad filesystem access. It is defense in depth. The input still needs authentication, schema, and resource checks, and the worker needs a bounded lifecycle.

What belongs in the review record?

Retain the data contract, producer and consumer trust assumptions, parser and runtime versions, authentication and filter policy, size and graph limits, fixture IDs, event-order assertions, side-effect results, timeout cleanup, and the final accept or reject decision. Exclude raw payloads, keys, credentials, object graphs, and uncontrolled exception text.

Reader next step

Choose one deserialization path in the next coding-agent patch and write its contract before changing the implementation. Replace native object reconstruction with a plain, versioned data record if possible. For a deliberately retained legacy protocol, verify authentication before object reconstruction, configure supported type and resource restrictions before the first object read, and isolate the worker. Replace BinaryFormatter and any pickle path accepting untrusted or tamperable data.

Add the valid, malformed, type-marker, signature, oversized, deep-graph, callback, and timeout fixtures from the matrix. Require evidence that rejection happened before construction and that no downstream side effect occurred. Block the merge if the patch checks types only after deserialization, falls back to a permissive loader, accepts unbounded graphs, or logs raw serialized input. Once the focused boundary passes, run the repository’s normal security and regression gates and attach only sanitized evidence.