mutated × unstructured — blows up live on input you didn't expect
Schema-Drift on Load: Old Payload Sets Unexpected Attributes
A record serialized by an older class version is rehydrated by a mixin/ORM that blindly sets whatever attributes the payload names -- so a renamed or dropped field lands on the new object and a later access breaks.
01the recipe
In the wild
compound ofMacro & Mixin MisuseInsecure DeserializationCWE-502 Unsafe DeserializationcompoundCWE-915 Object Attribute Injection
example.py
# SMELL: a mixin rehydrates rows by setting every key from the payload.
# (macro & mixin misuse x insecure deserialization)
class FromDict: # shared across all models
@classmethod
def load(cls, d):
o = cls.__new__(cls)
o.__dict__.update(d) # sets WHATEVER keys the old schema had
return o
# v1 stored {"addr": ...}; v2 renamed it "address" -> o.address is missing
# and a stale "addr" rides along. Later o.address -> AttributeError.
# RIGHT: map through a versioned, allow-listed schema on load.
class FromDict:
@classmethod
def load(cls, d):
d = migrate(d, d.get("_v", 1)) # bring the old shape to current
return cls(**{k: d[k] for k in cls.FIELDS}) # only known fieldsBlindly splatting a deserialized dict onto an object (CWE-502 feeding mass-assignment, CWE-915) couples your data to whatever schema wrote it; after a field is renamed across versions, old payloads set unexpected attributes and omit current ones, so a downstream access fails. The contract that drifted is the schema version between writer and reader. Migrate by version and assign only allow-listed fields.
// observed
drifted: old rows leave .address unset (+ a stale .addr) -> AttributeError right: versioned migration + field allow-list -> every object is current-shape
02weakness 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.