Vulnerability Taxonomy
Class pollution consists of multiple object access steps followed by a final object assignment step. Each access or assignment step is a pollution primitive, and each primitive is built from one or more atomic “get” or “set” operations.
Our taxonomy separates this structure into two layers:
- Get & Set Atomics: the individual reflective operations Python supports for reading and writing object state, e.g.,
getattr(obj, name),obj.__dict__[name],dict[key],setattr(obj, name, val). - Pollution Primitives: the attacker’s capability at each step, expressed in terms of which atomics they can use there. The “get” primitive is either Agnostic (the program allows free choice between attribute and item access) or Constrained (program logic fixes one atomic). The “set” primitive is Dual, Attr-only, or Item-only. The 2 × 3 combination yields the six class pollution variants.
Why this classification?
Two programs that look superficially similar can have very different pollution capability. Consider these two instructive code snippets, both “reflective writes from user input”:
Program A:
def update(obj, data):
for k, v in data.items():
if isinstance(v, dict):
if isinstance(obj, dict):
update(obj[k], v)
else:
update(getattr(obj, k), v)
else:
if isinstance(obj, dict):
obj[k] = v
else:
setattr(obj, k, v)
Program B:
def deep_set(obj, dotted, value):
parts = dotted.split(".")
for part in parts[:-1]:
obj = getattr(obj, part)
setattr(obj, parts[-1], value)
The two programs differ along two axes:
- Get capability (target object access path): Every Python object exposes two namespaces, the attribute namespace (read by
getattr/obj.x) and the item namespace (read byobj[k]). They are not interchangeable: an attribute cannot be retrieved with item lookup, and a dict entry cannot be retrieved with attribute access. So whether the program lets the attacker mix the two at each step determines what they can reach. Program A can traverse mappings and attributes, so it can step from a dict into a class via__class__, or from a class into a module via__init__.__globals__. Program B is locked to attribute-only walks and cannot enter or escape a container. - Set capability (final write target): Program A can finish with either
setattrorobj[k] = v, while Program B only withsetattr. The choice of final write determines what kinds of targets are reachable: an attribute-write lands on classes, modules, and functions, while an item-write lands on container internals such as__globals__andos.environ.
Classifying by primitive (what the attacker can choose at each step) captures these differences precisely. Of the six variants, only Agnostic-Get × Dual-Set was shown before and the other five were introduced in our IEEE S&P 2026 paper.
Capability matrix
The two columns under target object access path describe what the attacker can use to reach the target during traversal; the two columns under target object types describe what kinds of objects the final write can land on. A “Yes” means the variant supports that path or target; a “No” means the program shape forbids it.
| Variant Types | Target Object Access Path | Target Object Types | ||
|---|---|---|---|---|
| GetAttr only | GetItem & GetAttr | Containers (dict, list) |
General objects | |
| Agnostic-Get × Dual-Set | Yes | Yes | Yes | Yes |
| Agnostic-Get × Attr-Set | Yes | Yes | No | Yes |
| Agnostic-Get × Item-Set | Yes | Yes | Yes | No |
| Constrained-Get × Dual-Set | Yes | No | Yes | Yes |
| Constrained-Get × Attr-Set | Yes | No | No | Yes |
| Constrained-Get × Item-Set | Yes | No | Yes | No |
Reading the table:
- The first two columns track the get capability. “GetAttr only” is always available because attribute access works on every Python object. The “GetItem & GetAttr” column is what separates Agnostic-Get (the program lets the attacker reach through containers) from Constrained-Get (it does not).
- The last two columns track the set capability. Attr-Set can land writes on classes, modules, functions, and other general objects, but not on container internals. Item-Set is the inverse: it can only modify mapping/sequence entries. Dual-Set combines both.
The variant that “Yes everything” is Agnostic-Get × Dual-Set — the only variant prior work covered. The other five each carve off a strict subset of the capability surface, which is why they need different detection logic and different gadgets to exploit.