Security Finding
Severity: medium
Type: unsafe-pattern (authorization/integrity regression)
store.Idea.TransitionTo (added in #160, commit 7a5cfea) treats a same-status transition as a silent no-op success. API.accept in pkg/api/wave2.go previously relied on CanTransition(accepted, accepted) == false to reject a second acceptance; after the refactor that rejection is gone.
When an ideator offers an idea to two repos and repo A accepts (default matchmaker mode leaves the idea in status accepted), repo B's offer is still pending, so B's owner can call POST /api/repos/{B}/decide with decision=accept. The guard offer == nil && status != draft/offered passes, TransitionTo(accepted) no-ops successfully, and the mutate closure overwrites TargetRepo to repo B and marks B's offer accepted.
Reproduced by test: second accept returned 200 and flipped targetRepo from kubestellar/dibs to org/other.
Impact
Any repo owner holding a still-pending offer can hijack an already-accepted idea: the credited GitHub issue (prefilled new-issue URL / legacy server-side settle) is redirected to their repo, stealing the acceptance and credit from the repo that legitimately accepted first, and corrupting the idea's offer records.
Recommendation
In accept()'s mutate closure, explicitly reject when i.Status == store.StatusAccepted before calling TransitionTo, restoring the pre-#160 behavior. Add a regression test for the two-repo double-accept path. (Settle/confirm paths are unaffected: handleConfirmIssue has an explicit status guard.)
Filed by sec-check agent (ACMM L6 — full mode)
🐝 Hive Agent: security | Instance: hosted-available-oke-11-placeholder-r05x | SHA: unknown
— hive: agent=sec-check backend=copilot model=claude-fable-5 copilot=1.0.88
Security Finding
Severity: medium
Type: unsafe-pattern (authorization/integrity regression)
store.Idea.TransitionTo(added in #160, commit 7a5cfea) treats a same-status transition as a silent no-op success.API.acceptinpkg/api/wave2.gopreviously relied onCanTransition(accepted, accepted) == falseto reject a second acceptance; after the refactor that rejection is gone.When an ideator offers an idea to two repos and repo A accepts (default matchmaker mode leaves the idea in status
accepted), repo B's offer is stillpending, so B's owner can callPOST /api/repos/{B}/decidewithdecision=accept. The guardoffer == nil && status != draft/offeredpasses,TransitionTo(accepted)no-ops successfully, and the mutate closure overwritesTargetRepoto repo B and marks B's offer accepted.Reproduced by test: second accept returned 200 and flipped
targetRepofromkubestellar/dibstoorg/other.Impact
Any repo owner holding a still-pending offer can hijack an already-accepted idea: the credited GitHub issue (prefilled new-issue URL / legacy server-side settle) is redirected to their repo, stealing the acceptance and credit from the repo that legitimately accepted first, and corrupting the idea's offer records.
Recommendation
In
accept()'s mutate closure, explicitly reject wheni.Status == store.StatusAcceptedbefore callingTransitionTo, restoring the pre-#160 behavior. Add a regression test for the two-repo double-accept path. (Settle/confirm paths are unaffected:handleConfirmIssuehas an explicit status guard.)Filed by sec-check agent (ACMM L6 — full mode)
🐝 Hive Agent:
security| Instance:hosted-available-oke-11-placeholder-r05x| SHA:unknown— hive: agent=sec-check backend=copilot model=claude-fable-5 copilot=1.0.88