Skip to content

CapabilityGrant.parse_capability truncates 4+ segment capabilities, enabling parent/sibling privilege escalation #3180

Description

@liamcrumm

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.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions