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.
In the wild
Hydrating Untrusted Bytes
A native deserializer reconstructs objects — and can run their code — before any of your logic sees the data.
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 codepickle: __reduce__ runs -> shell command schema: bad shape -> ValidationError, no code
// 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 typereadObject: 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.
# 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 inputmass assign: admin: true -> account escalated allowlist: only name/email accepted
// 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 untouchedassign body: role -> 'admin' (overwritten) pick fields: role unchanged, name/email set
Where this compounds
- Deserialized Type Confusion Crash × this Mutation
- Schema-Drift on Load: Old Payload Sets Unexpected Attributes × Macro & Mixin Misuse
- Untrusted Deserialization Across a Boundary × File & Network Access
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.