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

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 fields
Blindly 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.