Skip to content

Bug: get_implicit_permissions_for_user returns references to internal policy rows, mutating the enforcer store #424

@killmesoonbaby044

Description

@killmesoonbaby044

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

Root 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 res

get_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 row

The 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions