Skip to content

fix(assessment): bulk sign-off/confirm cannot bypass required-signer role or N-of-M - #41

Open
simonorzel26 wants to merge 1 commit into
mainfrom
fix/bulk-signoff-role-enforcement
Open

fix(assessment): bulk sign-off/confirm cannot bypass required-signer role or N-of-M#41
simonorzel26 wants to merge 1 commit into
mainfrom
fix/bulk-signoff-role-enforcement

Conversation

@simonorzel26

Copy link
Copy Markdown
Contributor

Security gap (pre-existing, surfaced by the sign-off e2e review)

bulkSignOffCategory and bulkConfirmModuleRef each write a completed sign-off (status=completed, signedOffBy/signedOffRole, chain entry) for every matching requirement — but neither checked requirement.requiredSignOffRole or existing N-of-M assignments. The single signOff path does both (assessment.ts:564 FORBIDs a non-CEO from signing a CEO-required requirement; N-of-M records one signature toward the quorum).

Impact: an authenticated category owner (member) could complete the §38 CEO duties — 1.3 budget, 1.4 personal liability, 2.4 risk acceptance, plus module-backed 4.2/7.3 — by calling a bulk endpoint instead of signOff, defeating the dual-control the product's evidentiary claim rests on. Neither endpoint is UI-wired, but both are reachable companyProcedures (any tenant member with a session).

Fix

Both endpoints now filter to signable rows before the (unchanged) write loop. A row is skipped when:

  • it has assignment rows → its assigned signers must each sign through the assignment flow (no N-of-M shortcut), or
  • the caller's signer role does not satisfy the requirement's requiredSignOffRole — same admin-bypass rule signOff uses.

The audited chain-write path (B-2) is untouched; only the row set narrows. Redundant second getSignerRole call in bulkConfirmModuleRef collapsed into the one the filter already computes.

Review asks

  • Security-sensitive, touches the sign-off core — please review the row-filter semantics (skip-vs-refuse choice: I chose skip-unauthorized so a category owner still bulk-signs what they legitimately can).
  • Follow-up: a tRPC-level authz test asserting a member cannot bulk-complete a CEO-required requirement (browser e2e can't reach these unwired endpoints).

Typecheck clean. Not merging without your go.

🤖 Generated with Claude Code

https://claude.ai/code/session_01De1NY1HDUJkkwetVKTbYtH

…role or N-of-M

bulkSignOffCategory and bulkConfirmModuleRef both write a completed
sign-off (status=completed, signedOffBy/signedOffRole, chain entry) for
every matching requirement, but neither checked requirement.requiredSignOffRole
or existing N-of-M assignments — unlike the single signOff path, which
FORBIDs a non-CEO from signing a CEO-required requirement and records
one signature toward the assigned quorum. So an authenticated category
owner could complete the §38 CEO duties (1.3/1.4/2.4, plus module-backed
4.2/7.3) by calling the bulk endpoints instead of signOff. Neither
endpoint is wired to the UI today, but both are reachable companyProcedures.

Both now filter to signable rows before writing: a row is skipped if it
has assignment rows (its assigned signers must each sign through the
assignment flow) or if the caller's signer role does not satisfy the
requirement's requiredSignOffRole — same admin-bypass rule signOff uses.
The chain-write path (audit B-2) is unchanged; only the row set narrows.

Pre-existing gap, surfaced by the e2e sign-off review (my specs covered
only the single path). Security-sensitive: recommend Cory reviews, and a
tRPC-level authz test asserting a member cannot bulk-complete a
CEO-required requirement.
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.

1 participant