Approval quorum attributes approvals to the approver named in the request body
services/policy/approvals/handler.go:L74–L121 (RecordApproval)
- Likelihood
- Medium
- Impact
- High
- Weakness
- CWE-639
- Description
The console posts approvals to the policy engine as JSON containing
request_id,approver_idand the WebAuthn assertion.RecordApprovalverifies the assertion against the session's registered credential, then stores the approval underbody.ApproverID. The verified credential is never compared withapprover_id.An approver can therefore submit their own valid assertion several times with different
approver_idvalues. Each one is stored as a distinct approval, and the quorum check counts distinctapprover_ids, so a 3-of-5 policy can be satisfied by one person.The researcher programme for Northstar lists this outcome as high severity. It does not by itself allow signing outside policy, because whitelists and limits still apply, but quorum is the main control for large or unusual transfers.
- Preconditions
- A valid approver account with a registered hardware key.
- Knowledge of other approvers' IDs, which the console shows in the approval list.
- Exploit scenario
- 1.A malicious approver creates or waits for a large transfer to a whitelisted address that requires three approvals.
- 2.They approve once in the console, then replay the request three times with the IDs of two colleagues, reusing a fresh assertion from their own key each time.
- 3.The policy engine records three approvals and issues a signing authorisation.
- Impact
A single approver can authorise transactions that require several, defeating separation of duties.
- Recommendation
Derive the approver identity from the authenticated session and the credential verified in the assertion; ignore any identity in the request body. Include the request ID and transaction digest in the WebAuthn challenge.
- Fix
- services/policy/approvals/handler.go (remediation commit)diff
- cred, err := h.webauthn.Verify(sess, body.Assertion)+ cred, err := h.webauthn.Verify(sess, body.Assertion, challengeFor(body.RequestID, req.Digest)) if err != nil { return ErrBadAssertion }- return h.store.AddApproval(body.RequestID, body.ApproverID)+ approver, err := h.users.ByCredential(cred.ID)+ if err != nil || approver.ID != sess.UserID {+ return ErrApproverMismatch+ }+ return h.store.AddApproval(body.RequestID, approver.ID) - Resolution
Fixed and verified. The approver is resolved from the verified credential and must match the session; the challenge binds the request and digest. Property P3 holds on the remediated model.
