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"]] ← WRONG
Reproducible with both casbin.Enforcer (sync) and casbin.AsyncEnforcer (async).
Root Cause
The full call chain for the async case (same path exists in the sync enforcer):
AsyncEnforcer.get_implicit_permissions_for_user()
→ get_named_implicit_permissions_for_user() # async_enforcer.py
→ get_named_permissions_for_user_in_domain() # async_enforcer.py
→ get_filtered_named_policy() # management_enforcer.py
→ get_filtered_policy() # model/policy.py ← root cause
The bug is in get_filtered_policy in casbin/model/policy.py:
def get_filtered_policy(self, sec, ptype, field_index, *field_values):
return [
rule # ← returns the actual list object from internal storage, not a copy
for rule in self[sec][ptype].policy
if all(
(callable(value) and value(rule[field_index + i])) or (value == "" or rule[field_index + i] == value)
for i, value in enumerate(field_values)
)
]
The list comprehension returns the same list objects that are stored in self[sec][ptype].policy. No copy is made at any point in the chain. As a result, the caller receives direct aliases into the enforcer's internal storage, and any in-place mutation immediately corrupts it.
This propagates up through get_named_implicit_permissions_for_user:
for role in roles:
permissions = await self.get_named_permissions_for_user_in_domain(ptype, role, ...)
res.extend(permissions) # ← extends with direct references into self.model
return res
This affects all methods that go through get_filtered_policy:
get_implicit_permissions_for_user()
get_named_implicit_permissions_for_user()
get_permissions_for_user()
get_filtered_policy() (directly)
Expected Behavior
The returned list should be fully independent of the enforcer's internal state. Modifying it must not affect the enforcer.
Proposed Fix
The fix belongs in get_filtered_policy in casbin/model/policy.py, so all callers are protected automatically. Change rule to rule[:]:
def get_filtered_policy(self, sec, ptype, field_index, *field_values):
return [
rule[:] # ← shallow copy; safe since policy rows are list[str] and str is immutable
for rule in self[sec][ptype].policy
if all(
(callable(value) and value(rule[field_index + i])) or (value == "" or rule[field_index + i] == value)
for i, value in enumerate(field_values)
)
]
rule[:] (shallow slice copy) is sufficient — policy rows are list[str] and strings are immutable in Python, so a one-level copy fully isolates the caller. copy.deepcopy is not needed and would add unnecessary overhead on every policy query.
Fixing it here covers all affected methods at once without touching each caller individually.
Workaround (until fixed)
Deep-copy the result immediately after calling:
import copy
perms = copy.deepcopy(await enforcer.get_implicit_permissions_for_user(user))
Or, if you are transforming the result into typed objects anyway, simply never mutate the raw rows in-place:
# Safe — constructs new objects without touching the original 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
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. Subsequentenforce()calls will produce incorrect results without any error or warning.Steps to Reproduce
Reproducible with both
casbin.Enforcer(sync) andcasbin.AsyncEnforcer(async).Root Cause
The full call chain for the async case (same path exists in the sync enforcer):
The bug is in
get_filtered_policyincasbin/model/policy.py:The list comprehension returns the same
listobjects that are stored inself[sec][ptype].policy. No copy is made at any point in the chain. As a result, the caller receives direct aliases into the enforcer's internal storage, and any in-place mutation immediately corrupts it.This propagates up through
get_named_implicit_permissions_for_user:This affects all methods that go through
get_filtered_policy:get_implicit_permissions_for_user()get_named_implicit_permissions_for_user()get_permissions_for_user()get_filtered_policy()(directly)Expected Behavior
The returned list should be fully independent of the enforcer's internal state. Modifying it must not affect the enforcer.
Proposed Fix
The fix belongs in
get_filtered_policyincasbin/model/policy.py, so all callers are protected automatically. Changeruletorule[:]:rule[:](shallow slice copy) is sufficient — policy rows arelist[str]and strings are immutable in Python, so a one-level copy fully isolates the caller.copy.deepcopyis not needed and would add unnecessary overhead on every policy query.Fixing it here covers all affected methods at once without touching each caller individually.
Workaround (until fixed)
Deep-copy the result immediately after calling:
Or, if you are transforming the result into typed objects anyway, simply never mutate the raw rows in-place:
Environment