digitalbazaar / digitalbazaar/zcap
Local delegation validation silently skips the `allowedAction` check when `parentCapability` is a string
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 29
- Forks
- 4
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 1
Description
Summary
CapabilityDelegation accepts parentCapability as either a full capability object or "a string expressing the ID of a root capability" (lib/CapabilityDelegation.js:97). When the string form is used, the local allowedAction restrictiveness check in update() reads the property off that string, gets undefined, and passes vacuously. A delegator can therefore sign a capability that is less restrictive than its root, with no error, and only discover the problem when a verifier rejects it.
Mechanism
lib/CapabilityDelegation.js:158:
const {allowedAction: parentAllowedAction} = parentCapability;
For a string this is undefined, so utils.hasValidAllowedAction short-circuits on if(!parentAllowedAction) return true and the check never runs.
A root capability is precisely the case where this matters. checkCapability permits allowedAction on root capabilities (the check at lib/utils.js:567 sits outside the root/delegated branches), and #90 added enforcement of a root's allowedAction at verification time. So the string form is the one path where a delegator can set allowedAction against a root that constrains it and get no local feedback.
Note that the neighboring expires check is not affected: checkCapability forbids expires on root capabilities (lib/utils.js:528), so parentExpires is legitimately undefined for a root whether it is passed by ID or as an object.
Reproduction
Root gamma declares allowedAction: 'write'. Delegating from its ID string with allowedAction: ['read', 'write'] signs cleanly, then fails verification:
const zcap = await _delegate({
newCapability: {
'@context': ZCAP_CONTEXT_URL,
id: uuid(),
controller: bob.id(),
parentCapability: capabilities.root.gamma.id,
invocationTarget: capabilities.root.gamma.invocationTarget,
allowedAction: ['read', 'write'],
expires: EXPIRES_3000_DATE
},
// string form of the root zcap
parentCapability: capabilities.root.gamma.id,
delegator: alice
});
// no error thrown
const r = await _verifyDelegation({
delegation: zcap,
expectedRootCapability: capabilities.root.gamma.id
});
// r.verified === false
// 'The "allowedAction" in a delegated capability must not be less restrictive
// than its parent.'
Passing capabilities.root.gamma (the object) instead throws at signing time, as intended.
Impact
This is a correctness and developer-experience problem rather than a security hole: verification catches the bad capability, so nothing invalid is accepted. But it lets a delegator mint capabilities that are dead on arrival, and the failure surfaces far from its cause. That matters more now that #90 makes a root's allowedAction load-bearing at verification time - a case that previously failed silently and harmlessly now produces unusable capabilities.
Options
-
Dereference the root capability during local validation and check against the real document.
documentLoaderis available toupdate()(it is currently destructured away -lib/CapabilityDelegation.js:122takes only{document}). This is the correct fix, but it introduces loader I/O into proof creation where there is none today, and makes delegation fail when the root is unreachable. That tradeoff deserves a deliberate decision. -
Reject the string form outright and require the full parent capability object. Simplest and safest, but a breaking API change.
-
Document the limitation and leave local validation as best-effort.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/CapabilityDelegation.js:97, 122, and 158, then read utils.hasValidAllowedAction and the root-capability checks around lib/utils.js:528 and 567. Run the reproduction using a string parentCapability and compare it with the object form. Done means the project has an agreed behavior for string roots, local validation matches verification, and the regression is covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- authorization, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100