untrusted bytes become live objects

Insecure Deserialization

Deserialization is the purest form of consuming unknown input as state: bytes you did not create become objects with behavior. Native formats can execute code on the way in (chaining to RCE, CWE-94/78), and even 'safe' formats over-assign attributes the caller never meant to expose.

Hydrating untrusted input straight into live objects runs code and sets state the caller never wrote.

01in the wild

In the wild

Hydrating Untrusted Bytes

A native deserializer reconstructs objects — and can run their code — before any of your logic sees the data.

example.py
import pickle

# DANGER: pickle reconstructs arbitrary objects -- and runs __reduce__
data = pickle.loads(untrusted_bytes)   # attacker payload = RCE

# An attacker's class can do this on load:
class Exploit:
    def __reduce__(self):
        return (os.system, ("rm -rf /",))   # runs during loads()

# RIGHT: a data-only format + an explicit schema
import json
from pydantic import BaseModel
class Order(BaseModel): id: int; qty: int
order = Order(**json.loads(untrusted_bytes))   # validated, no code
pickle.loads will execute an object's __reduce__ as it rebuilds it, so loading attacker bytes is arbitrary code execution. Parse a data-only format and validate against a schema instead.
// observed
pickle: __reduce__ runs -> shell command
schema: bad shape -> ValidationError, no code
example.java
// DANGER: readObject rebuilds any serializable type on the classpath
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object o = in.readObject();        // gadget chains -> RCE

// RIGHT: don't deserialize untrusted native streams.
// Use a data format with a known target type:
ObjectMapper m = new ObjectMapper();
m.activateDefaultTyping(...);      // <- do NOT; this re-opens the hole
Order order = m.readValue(json, Order.class);   // fixed target type
Java's native readObject instantiates whatever the stream names, letting gadget chains reach dangerous code. Avoid native serialization for untrusted data; bind a known type from a data format.
// observed
readObject: gadget chain -> code execution
readValue:  bound to Order.class only

Over-Permissive Hydration

Splatting raw input onto an object sets fields the caller never intended — like a privilege flag.

example.rb
# WRONG: mass assignment trusts every key in the payload
user = User.new(params)         # params has admin: true -> privilege escalation
user.save

# RIGHT: strong params -- an explicit allowlist of fields
permitted = params.require(:user).permit(:name, :email)
user = User.new(permitted)      # admin can't be set from input
Handing the whole params hash to a model lets a caller set any attribute, including admin. An allowlist (strong params) makes the writable set explicit.
// observed
mass assign: admin: true -> account escalated
allowlist:   only name/email accepted
example.js
// WRONG: copy every field from the request onto the record
Object.assign(user, req.body);   // req.body.role = "admin" sticks
await user.save();

// RIGHT: pick only the fields you mean to accept
const { name, email } = req.body;
Object.assign(user, { name, email });   // role is untouched
Object.assign with raw req.body lets the client overwrite any property, including role. Destructure the known-safe fields and assign only those.
// observed
assign body: role -> 'admin' (overwritten)
pick fields: role unchanged, name/email set
02cross-pollination

Where this compounds

03weakness catalog

Mapped weaknesses (CWE)

On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.