Summary
POST /api/v1/bounties/{id}/claim (claim_bounty, crates/gitlawb-node/src/api/bounties.rs) resolves a bounty by its global id and returns the full BountyRecord to any authenticated agent, with no authorize_repo_read on the bounty's repo. Its only precondition is status == "open" (bounties.rs:171). So an agent who is neither the owner nor a permitted reader of a private repo can claim that repo's open bounty and receive repo_owner, repo_name, title, issue_id, and creator_did — and, as a side effect, becomes claimant_did and flips the bounty to claimed, locking it against the legitimate claimant.
This is the write-side sibling of #120. #157 gates the read surfaces (get_bounty, list_all_bounties, list_repo_bounties); once that lands, claim_bounty is the remaining ungated path to a private bounty's metadata, and the only one that also mutates state.
Scope
Only claim_bounty. The other mutations enforce a did_matches creator/claimant check before returning the record, so they don't disclose to an outsider:
submit_bounty — claimant-only (bounties.rs:213-219)
approve_bounty — creator-only (bounties.rs:253-257)
cancel_bounty — creator-only (bounties.rs:300-304)
dispute_bounty — creator-or-claimant (bounties.rs:338-347)
create_bounty already gates via authorize_repo_read (bounties.rs:84). The route group bounty_write_routes requires a signature (require_signature), i.e. any authenticated agent, not the repo owner/reader.
Reproduction (executed)
Seed a private repo + an open bounty on it, then have a different authenticated DID POST /api/v1/bounties/{id}/claim:
status=200
{"repo_owner":"did:key:zCLAIMOWNER…","repo_name":"secret-repo","issue_id":"SECRET-ISSUE",
"title":"SECRET-TITLE","creator_did":"did:key:zCLAIMOWNER…","status":"claimed",
"claimant_did":"did:key:zCLAIMSTRANGER…", …}
The stranger is neither owner nor reader; they get the private repo's owner/name/title and have locked the bounty.
Fix (verified)
After resolving the bounty and before the status check, authorize the caller against the bounty's repo, mirroring create_bounty (bounties.rs:84):
crate::api::authorize_repo_read(&state, &bounty.repo_owner, &bounty.repo_name, Some(auth.0.as_str()), "/").await?;
authorize_repo_read returns RepoNotFound (404) on deny, so a non-reader gets the same 404 as a missing bounty (no existence oracle). With this in place, the stranger claim above returns 404 with no body, while the repo owner claiming their own bounty still returns 200 (verified).
Add a deny test (non-reader claiming an open bounty on a private repo → 404) plus an owner-admit test. Note the authz_guard drift test can't catch this class — it keys on Path<(owner, repo)> and claim_bounty is a global-id handler.
Summary
POST /api/v1/bounties/{id}/claim(claim_bounty,crates/gitlawb-node/src/api/bounties.rs) resolves a bounty by its global id and returns the fullBountyRecordto any authenticated agent, with noauthorize_repo_readon the bounty's repo. Its only precondition isstatus == "open"(bounties.rs:171). So an agent who is neither the owner nor a permitted reader of a private repo can claim that repo's open bounty and receiverepo_owner,repo_name,title,issue_id, andcreator_did— and, as a side effect, becomesclaimant_didand flips the bounty toclaimed, locking it against the legitimate claimant.This is the write-side sibling of #120. #157 gates the read surfaces (
get_bounty,list_all_bounties,list_repo_bounties); once that lands,claim_bountyis the remaining ungated path to a private bounty's metadata, and the only one that also mutates state.Scope
Only
claim_bounty. The other mutations enforce adid_matchescreator/claimant check before returning the record, so they don't disclose to an outsider:submit_bounty— claimant-only (bounties.rs:213-219)approve_bounty— creator-only (bounties.rs:253-257)cancel_bounty— creator-only (bounties.rs:300-304)dispute_bounty— creator-or-claimant (bounties.rs:338-347)create_bountyalready gates viaauthorize_repo_read(bounties.rs:84). The route groupbounty_write_routesrequires a signature (require_signature), i.e. any authenticated agent, not the repo owner/reader.Reproduction (executed)
Seed a private repo + an open bounty on it, then have a different authenticated DID
POST /api/v1/bounties/{id}/claim:The stranger is neither owner nor reader; they get the private repo's owner/name/title and have locked the bounty.
Fix (verified)
After resolving the bounty and before the status check, authorize the caller against the bounty's repo, mirroring
create_bounty(bounties.rs:84):authorize_repo_readreturnsRepoNotFound(404) on deny, so a non-reader gets the same 404 as a missing bounty (no existence oracle). With this in place, the stranger claim above returns 404 with no body, while the repo owner claiming their own bounty still returns 200 (verified).Add a deny test (non-reader claiming an open bounty on a private repo → 404) plus an owner-admit test. Note the
authz_guarddrift test can't catch this class — it keys onPath<(owner, repo)>andclaim_bountyis a global-id handler.