Summary
CapabilityGrant.parse_capability() truncates capability strings to three components, silently dropping everything after the second colon. This causes a privilege escalation for capabilities with 4+ segments that is independent of, and not fixed by, #3176.
Root cause
parse_capability (agent-governance-python/agent-mesh/src/agentmesh/trust/capability.py) does:
parts = capability.split(":")
action = parts[0]
resource = parts[1]
qualifier = parts[2] if len(parts) > 2 else None # parts[3:] are discarded
So write:database:table_users:row_1 parses to ('write', 'database', 'table_users') — the row_1 segment is lost. The grant is stored and compared as if it were write:database:table_users.
Impact (privilege escalation)
Because the 4th+ segment is dropped, a grant scoped to one leaf authorizes its parent and its siblings:
reg = CapabilityRegistry()
reg.grant("write:database:table_users:row_1", "child", "admin")
reg.check("child", "write:database:table_users") # True (parent — escalation)
reg.check("child", "write:database:table_users:row_2") # True (sibling — escalation)
Verified identical on main and on the #3176 branch, so this is pre-existing, not introduced by #3176. #3176 closes the 3-segment qualifier and resource_ids escalation; this 4+-segment variant remains open because it lives in the parser, not the matcher.
Why it needs its own change (not folded into #3176)
A correct fix changes the capability data model: qualifier would need to capture the full remainder (table_users:row_1) or parse_capability would need to return a variable-length component list. That ripples into the colon-boundary prefix branch in matches(), create(), and any stored/serialized grants — a security-model change with its own blast radius that warrants a maintainer decision and a dedicated test matrix.
Proposed direction (for maintainer input)
Either (a) preserve the full sub-resource path (qualifier = ":".join(parts[2:])) and make the component comparison hierarchy-aware, or (b) reject capabilities with more than three components at grant() time (fail-closed) until hierarchical qualifiers are designed. Option (b) is the smaller, safer interim guard.
Filed from a deep code-review pass on #3176. The escalation reproduces on main; the repro above is runnable from agent-governance-python/agent-mesh with PYTHONPATH=src.
Summary
CapabilityGrant.parse_capability()truncates capability strings to three components, silently dropping everything after the second colon. This causes a privilege escalation for capabilities with 4+ segments that is independent of, and not fixed by, #3176.Root cause
parse_capability(agent-governance-python/agent-mesh/src/agentmesh/trust/capability.py) does:So
write:database:table_users:row_1parses to('write', 'database', 'table_users')— therow_1segment is lost. The grant is stored and compared as if it werewrite:database:table_users.Impact (privilege escalation)
Because the 4th+ segment is dropped, a grant scoped to one leaf authorizes its parent and its siblings:
Verified identical on
mainand on the #3176 branch, so this is pre-existing, not introduced by #3176. #3176 closes the 3-segment qualifier andresource_idsescalation; this 4+-segment variant remains open because it lives in the parser, not the matcher.Why it needs its own change (not folded into #3176)
A correct fix changes the capability data model:
qualifierwould need to capture the full remainder (table_users:row_1) orparse_capabilitywould need to return a variable-length component list. That ripples into the colon-boundary prefix branch inmatches(),create(), and any stored/serialized grants — a security-model change with its own blast radius that warrants a maintainer decision and a dedicated test matrix.Proposed direction (for maintainer input)
Either (a) preserve the full sub-resource path (
qualifier = ":".join(parts[2:])) and make the component comparison hierarchy-aware, or (b) reject capabilities with more than three components atgrant()time (fail-closed) until hierarchical qualifiers are designed. Option (b) is the smaller, safer interim guard.Filed from a deep code-review pass on #3176. The escalation reproduces on
main; the repro above is runnable fromagent-governance-python/agent-meshwithPYTHONPATH=src.