Skip to content

fix(security): boundary-aware Capability.resourceCovers + fail-closed empty with (#585)#589

Merged
mikera merged 1 commit into
developfrom
security/ucan-capability-boundary
Jun 22, 2026
Merged

fix(security): boundary-aware Capability.resourceCovers + fail-closed empty with (#585)#589
mikera merged 1 commit into
developfrom
security/ucan-capability-boundary

Conversation

@mikera

@mikera mikera commented Jun 22, 2026

Copy link
Copy Markdown
Member

Fixes #585.

Vulnerability

Capability.resourceCovers matched a granted resource against a requested one with a raw startsWith and no path-segment boundary, so a grant on w/notes also covered siblings like w/notesSECRET / w/report-confidential — a UCAN attenuation / delegation escape (cross-tenant over-read/over-write where delegation is used). Separately, a null/empty with was treated as "covers every resource" (fail-open), turning a malformed or truncated capability into a superuser grant.

This is the companion to #586 (the JWT issuer-spoofing fix): #586 made the token layer sound; this makes the authorization-decision layer underneath it sound.

Fix

  • resourceCovers now requires a path-segment boundary on the prefix branch (mirroring the already-correct abilityCovers): the prefix matches only when the grant ends with / or the next char in the request is /. Exact-match and trailing-slash-parent are preserved; genuine children (w/notes/x) still match.
  • Both resourceCovers and covers now fail closed on null/empty grant or request with. An absent resource pointer is never a wildcard.
grant request before after
w/notes w/notes/child true ✅ true
w/notes w/notesSECRET true false
"" / null w/anything true false

Tests

CapabilityTest gains adversarial sibling-escape and empty/null-with superuser cases, plus genuine-child/exact regressions. The five existing tests that asserted the fail-open behaviour are flipped to expect fail-closed. Confirmed all seven adversarial assertions fail against the pre-fix code.

convex-core: 2135 pass.

🤖 Generated with Claude Code

… empty `with` (#585)

Capability.resourceCovers matched a granted resource against a requested one with a
raw string startsWith and no path-segment boundary, so a grant on `w/notes` also
covered siblings like `w/notesSECRET` / `w/report-confidential` — a UCAN attenuation
/ delegation escape (cross-tenant over-read/over-write where delegation is used).
Separately, a null/empty `with` was treated as "covers every resource" (fail-open),
turning a malformed or truncated capability into a superuser grant.

Fix:
- resourceCovers now requires a path-segment boundary on the prefix branch (mirroring
  the already-correct abilityCovers): the prefix matches only when the grant ends with
  '/' or the next char in the request is '/'. Exact match and trailing-slash-parent
  are preserved; genuine children (`w/notes/x`) still match.
- Both resourceCovers and covers now fail closed on null/empty grant or request `with`.
  An absent resource pointer is never a wildcard.

Tests (CapabilityTest): adversarial sibling-escape and empty/null-`with` superuser
cases, plus genuine-child/exact regressions. The five existing tests that asserted the
fail-open behaviour are flipped to expect fail-closed. Confirmed all seven adversarial
assertions fail against the pre-fix code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mikera mikera merged commit fa0fb5a into develop Jun 22, 2026
2 checks passed
@mikera mikera deleted the security/ucan-capability-boundary branch June 22, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Capability.resourceCovers ignores path-segment boundaries — UCAN attenuation escape (+ empty/absent with fails open)

1 participant