Skip to content

fix(recommendations): replace TOCTOU count+update with atomic RPC claim#224

Open
anshul23102 wants to merge 1 commit into
Coder-s-OG-s:mainfrom
anshul23102:fix/205-toctou-claim
Open

fix(recommendations): replace TOCTOU count+update with atomic RPC claim#224
anshul23102 wants to merge 1 commit into
Coder-s-OG-s:mainfrom
anshul23102:fix/205-toctou-claim

Conversation

@anshul23102
Copy link
Copy Markdown

Summary

Fixes #205.

`claimRecommendation` enforced the 3-active-claim limit with a SELECT COUNT followed by a separate UPDATE. Because these were two independent database round-trips, concurrent requests from the same user could both read a count below 3 before either write committed, allowing a user to hold more than 3 simultaneous claims.

Root cause

```typescript
// Step 1: read — concurrent requests both see count = 2
const { count: claimedCount } = await service.from('recommendations')
.select('id', { count: 'exact', head: true })
.eq('user_id', user.id)
.eq('status', 'claimed');

// Race window: another request sneaks in here

// Step 2: write — both proceed because they both passed Step 1
const { data } = await service.from('recommendations')
.update({ status: 'claimed', ... })
...
```

Fix

Migration (supabase/migrations/0012_claim_recommendation_atomic.sql)

A new PostgreSQL function claim_recommendation_atomic runs the count check and the status flip in a single UPDATE ... WHERE (SELECT count(*) ...) < 3 statement. PostgreSQL row-level locking ensures concurrent invocations serialise correctly.

Action (src/app/actions/recommendations.ts)

claimRecommendation now calls service.rpc('claim_recommendation_atomic', { p_rec_id, p_user_id }). When the RPC returns zero rows, a single count query distinguishes between claim_limit (at cap) and already_claimed (rec not open) for correct error codes.

Tests (src/app/actions/recommendations.test.ts)

  • mockServiceRpc added to the service mock.
  • claimRecommendation tests updated to use the RPC mock.
  • New tests for the zero-row claim_limit path, zero-row already_claimed path, and RPC error path.

Test plan

  • npx vitest run src/app/actions/recommendations.test.ts - 21/21 pass
  • npx tsc --noEmit - no errors
  • npx eslint src/app/actions/recommendations.ts src/app/actions/recommendations.test.ts - no errors

claimRecommendation enforced the 3-active-claim limit using a SELECT COUNT
followed by a separate UPDATE. Because these were two independent database
round-trips, concurrent requests from the same user could both pass the
count check before either write committed, allowing a user to hold more
than 3 simultaneous claims.

Fix:
- Adds supabase/migrations/0012_claim_recommendation_atomic.sql, which
  creates the PostgreSQL function claim_recommendation_atomic. The function
  runs the count check and the status flip as a single UPDATE ... WHERE
  (SELECT count(*) ...) < 3 statement. PostgreSQL row-level locking ensures
  concurrent calls serialise correctly with no application-level race window.
- Updates claimRecommendation to call service.rpc('claim_recommendation_atomic',
  ...) instead of the two-step COUNT + UPDATE. When the RPC returns zero
  rows the action falls back to a single count query to return the correct
  claim_limit vs. already_claimed error code to the caller.
- Updates recommendations.test.ts: replaces the old service.from chain mocks
  with mockServiceRpc mocks and adds tests for the zero-row claim_limit path,
  the zero-row already_claimed path, and the RPC error path.

Closes Coder-s-OG-s#205
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 25, 2026

@anshul23102 is attempting to deploy a commit to the codersogs-3057's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
Copy link
Copy Markdown

Hey @anshul23102

You have 6 open PRs right now. The limit is 3 at a time.

Please get your existing PRs merged or closed before opening new ones:

This PR will remain open but won't be reviewed until you're under the limit. See our Contributing Guidelines for details.

@anshul23102
Copy link
Copy Markdown
Author

Closed the two older duplicate PRs (#211, #212) and one PR temporarily queued (#223). Down to 3 open PRs now. This PR is ready for review.

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.

Bug: TOCTOU race condition in claimRecommendation allows bypassing the 3-active-claim limit

1 participant