Deserializing untrusted data with serde is much safer than with reflection based formats, because serde builds typed structures through compile time traits and cannot instantiate arbitrary classes. It is not risk free. Attackers can still send huge or deeply nested input, so cap size and depth, reject unknown fields, and validate the result.
Why is serde different from pickle or Java serialization?
CWE-502, deserialization of untrusted data, describes the core problem: a product deserializes untrusted data without ensuring the result is valid. The worst cases, in Python pickle and Java object streams, let the data name which types to build and which code runs along the way.
The serde project describes its design instead: it is built on Rust's trait system rather than runtime reflection. A type implements Serialize and Deserialize, usually through a derive. The input can only produce the Rust types your code asked for. There is no "give me any class by name" path in the normal API.
So the remote code execution pattern is largely absent. What remains are the other halves of CWE-502: invalid data, and resource exhaustion.
Where can it still go wrong?
Constructed example: an upload endpoint accepts a JSON document describing a report, parsed into Report { items: Vec<Item> }. An attacker sends a 2 GB body, or a document nested thousands of levels deep.
Three failure modes to design for:
- Size. A parser will happily build a very large
VecorStringif the input contains it. Memory exhaustion is a denial of service. - Depth. Recursive descent parsers use the stack. The serde_json documentation shows a recursion limit exists by default, and that the method to remove it,
disable_recursion_limit, is only available behind theunbounded_depthfeature, with a warning that you must then protect against stack overflow some other way. Do not enable that feature for untrusted input. Note too that recursive operations on the parsed result, such as your own walk or a Drop, can still overflow later. - Meaning. A value can be the right type and still wrong: a negative quantity, an empty id, an enormous count that drives a later loop.
How do you reduce the risk?
Limit the bytes before you parse. Read from the network through a bounded reader or set a body limit in your web framework, so a 2 GB request never reaches serde.
Be strict about shape. Serde's container attribute deny_unknown_fields makes deserialization error on unknown fields; the docs note that without it, unknown fields are ignored for self describing formats like JSON. Strictness catches typos and smuggled fields, though it cannot be combined with flatten.
Use enums with closed variants for tagged data. An internally tagged enum, with #[serde(tag = "type")], accepts only variants you defined, which is the type safe version of a polymorphic payload.
Validate after parsing. Convert the raw deserialized struct into a validated domain type through TryFrom, checking ranges, lengths and invariants. Keep unvalidated types out of the rest of the program.
What about binary formats?
Binary formats such as bincode, postcard and MessagePack carry length prefixes. A tiny message can claim a huge collection. Check whether the crate you use supports a configured size limit, and set one, rather than trusting defaults. Review the crate documentation for your chosen version, since configuration APIs differ between major releases.
Do not treat a binary format as a trust boundary either. Use an authenticated envelope if the data leaves your system and comes back. CWE-502 lists signing or HMAC of serialized data as a mitigation, with the caveat that client side protection can be bypassed if the client is compromised.
Does unsafe code change this?
If a deserializer or your own Visitor uses unsafe blocks, a malformed input can become memory corruption instead of an error. Search your dependency tree for crates with unsafe in the parsing path and keep them updated. cargo audit checks your lockfile against the RustSec advisory database.
What to do
- Enforce a maximum body size before parsing.
- Never enable
unbounded_depthin serde_json for untrusted input. - Add
deny_unknown_fieldsto structs that cross a trust boundary. - Use enums with explicit variants for polymorphic payloads.
- Parse into raw types, then convert with
TryFrominto validated types. - Configure size limits on binary format crates, after reading their docs.
- Run
cargo auditin CI and update parser crates promptly.
Safeguard's dependency scanning and reachability analysis can tell you whether a vulnerable parser crate in your lockfile is actually called by your code.
Quick check
Can serde deserialization execute code?
Not through the normal API, since types come from compile time implementations rather than names in the data. Bugs in parser crates, particularly in unsafe code, remain possible, so keep them patched.
Is deny_unknown_fields on by default?
No. For self describing formats like JSON, unknown fields are ignored unless you add the attribute.
Sources
- CWE-502: Deserialization of Untrusted Data, MITRE, fetched 18 October 2026
- Serde overview, serde.rs, fetched 18 October 2026
- Container attributes, serde.rs, fetched 18 October 2026
- serde_json Deserializer, docs.rs, fetched 18 October 2026
Last reviewed: 18 October 2026.