Functions
A Python function object exposes three independent access mechanisms for pollution: __globals__, __kwdefaults__, and __closure__. Each one routes the polluted value into program behavior through a different path. The __closure__ mechanism is novel to this work. The other two were known in prior literature.
Access mechanism: global variable reference via __globals__
When a function f is accessible, the attacker can modify f.__globals__["v"], where v is a global variable in the module where f is defined. Every function carries a reference to its defining module’s __dict__, so any accessible function exposes its module’s entire namespace, and through sys.modules any other loaded module.
# mod.py
TEMPLATE_DIR = "/srv/app/templates"
def render(name):
path = TEMPLATE_DIR + "/" + name # uses the global TEMPLATE_DIR
return open(path).read()
render("home.html") # reads /srv/app/templates/home.html
# Attacker payload (Item-Set on render.__globals__):
# render.__globals__["TEMPLATE_DIR"] = "/etc"
render("passwd") # reads /etc/passwd
This is the same effect as polluting Modules directly, but routed through a function instead of through a module reference.
Access mechanism: local variable reference via __kwdefaults__
When a function f has keyword-only parameters, their default values live in the dictionary f.__kwdefaults__. Modifying an entry changes the default value used by every subsequent call that does not pass the argument explicitly. Keyword-only parameters are the parameters defined after * in the signature (PEP 3102).
def render_template(name, *, autoescape=True, cache=True):
...
render_template("home.html") # autoescape=True, output is HTML-escaped
# Attacker payload (Item-Set on render_template.__kwdefaults__):
# render_template.__kwdefaults__["autoescape"] = False
render_template("home.html") # autoescape=False, output is rendered raw, XSS is now possible
__kwdefaults__ is a dictionary, so this access mechanism requires an Item-Set or Dual-Set primitive.
Access mechanism: local variable reference via __closure__
When a function g is accessible as a closure, the attacker can modify the captured variables through g.__closure__[i].cell_contents. Python closures store captured variables in cell objects rather than in the function’s own dict, so the cell content is the foothold.
def make_checker(allowed):
def is_allowed(user):
return user in allowed # reads the captured `allowed` set
return is_allowed
is_allowed = make_checker({"alice", "bob"})
is_allowed("eve") # returns False
# Attacker payload (Attr-Set on the captured cell):
# is_allowed.__closure__[0].cell_contents = {"eve"}
is_allowed("eve") # returns True, the access check is bypassed
The cells of a closure are addressed in capture order:
g.__closure__ # tuple of cell objects, in capture order
g.__closure__[0] # first captured variable's cell
g.__closure__[0].cell_contents # the actual captured value
See XSS gadgets, RCE gadgets, and authentication bypass gadgets for end-to-end exploitation chains that build on these mechanisms.