digitalbazaar / digitalbazaar/zcap

Local delegation validation silently skips the `allowedAction` check when `parentCapability` is a string

Open
#91 0 comments 0 reactions 0 assignees View on GitHub

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

  1. Dereference the root capability during local validation and check against the real document. documentLoader is available to update() (it is currently destructured away - lib/CapabilityDelegation.js:122 takes 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.

  2. Reject the string form outright and require the full parent capability object. Simplest and safest, but a breaking API change.

  3. Document the limitation and leave local validation as best-effort.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.