-
Notifications
You must be signed in to change notification settings - Fork 213
Bug: get_implicit_permissions_for_user returns references to internal policy rows, mutating the enforcer store #424
Description
Description
Calling get_implicit_permissions_for_user() (or its async counterpart) and then modifying the returned list elements silently corrupts the enforcer's in-memory policy store. Subsequent enforce() calls will produce incorrect results without any error or warning.
Steps to Reproduce
import casbin
e = casbin.Enforcer("model.conf", "policy.csv")
# policy: p, admin, data1, read
# policy: p, alice, data2, read
# g, alice, admin
perms = e.get_implicit_permissions_for_user("alice")
# perms == [["admin", "data1", "read"], ["alice", "data2", "read"]]
# Mutate the returned list — e.g. to transform/enrich data
perms[0][2] = "write"
# The enforcer's internal store is now corrupted:
print(e.get_policy())
# [["admin", "data1", "write"], ["alice", "data2", "read"]] ← WRONGRoot Cause
The call chain is:
get_implicit_permissions_for_user()
→ get_named_implicit_permissions_for_user()
→ get_named_permissions_for_user_in_domain() [per role]
→ get_filtered_named_policy()
→ iterates self.model["p"][ptype].policy
and appends matching rows directly (no copy)
The critical lines in enforcer.py:
for role in roles:
permissions = self.get_named_permissions_for_user_in_domain(ptype, role, ...)
res.extend(permissions) # ← appends references, not copies
return resget_filtered_named_policy yields the same list objects that are stored in self.model["p"][ptype].policy. They are not copied at any point in the chain. As a result, the returned list contains direct aliases into the enforcer's internal storage, and any in-place modification by the caller immediately corrupts it.
This affects all related methods that go through the same chain:
get_implicit_permissions_for_user()get_named_implicit_permissions_for_user()get_permissions_for_user()get_filtered_policy()
Expected Behavior
The returned list should be independent of the enforcer's internal state. Modifying it must not affect the enforcer.
Proposed Fix
The fix should be applied in get_filtered_named_policy in casbin/model/policy.py, so all callers are protected automatically. Change the filter loop to append copies of the matching rows instead of the rows themselves:
# Before
return [rule for rule in assertion.policy if <filter condition>]
# After
return [rule[:] for rule in assertion.policy if <filter condition>]Using rule[:] (shallow slice copy) is sufficient because policy rows are list[str] — strings are immutable, so a one-level copy fully isolates the caller. copy.deepcopy is not needed and would add unnecessary overhead.
Alternatively, the copy can be applied one level up in get_named_implicit_permissions_for_user:
for role in roles:
permissions = self.get_named_permissions_for_user_in_domain(ptype, role, ...)
res.extend([p[:] for p in permissions]) # copy each rowThe policy.py level is preferable since it fixes all callers at once.
Workaround (until fixed)
Users can protect themselves by deep-copying the result immediately:
import copy
perms = copy.deepcopy(e.get_implicit_permissions_for_user(user))Or, if transforming to typed objects, by never mutating the raw rows in-place:
# Safe — constructs new objects from raw data without touching the rows
result = [MyDTO(sub=row[0], obj=row[1], act=row[2]) for row in raw]Environment
- pycasbin version: v2.8.0
- Python version: v3.10.12
- Async enforcer: yes
- Async enforcer version: v1.17.0