Skip to content

fix: make daily export limit check atomic to prevent concurrent requests bypassing quota#347

Open
anshul23102 wants to merge 1 commit into
geturbackend:mainfrom
anshul23102:fix/255-toctou-race
Open

fix: make daily export limit check atomic to prevent concurrent requests bypassing quota#347
anshul23102 wants to merge 1 commit into
geturbackend:mainfrom
anshul23102:fix/255-toctou-race

Conversation

@anshul23102

Copy link
Copy Markdown
Contributor

Problem

The export quota check used a non-atomic pattern: get the current count from Redis, check if it's at the limit, then increment. Two concurrent requests could:

  1. Both read currentCount = 0
  2. Both pass the limit check 0 < maxExports
  3. Both call redis.incr(key), resulting in a count of 2
  4. Both succeed despite a daily quota of 1 (or 5 for pro)

Steps to reproduce (from issue):

  1. Set maxExports = 1
  2. Send two simultaneous POST requests to the export endpoint
  3. Both requests succeed with usage counts 1/1 and 2/1
  4. The quota is bypassed

Fix

Use a Lua script to atomically check and increment in a single Redis transaction. The script is executed on the server side, ensuring no interleaving between the check and increment:

if current >= maxExports then
    return rejected
else
    increment and return new count
end

Concurrent requests now correctly queue at the limit and are rejected once quota is reached.

Closes #255

…sts bypassing quota

The export limit check used a non-atomic pattern: read the current count
from Redis, check if it exceeds the daily maximum, then increment. Two
concurrent requests arriving simultaneously could both read the same
count (0 if it was a fresh day), both pass the limit check (0 < 1 or 5),
and both increment, resulting in a count of 2 despite a quota of 1 or 5.

Use a Lua script to atomically check and increment the quota counter in
a single Redis round-trip. The script is executed atomically on the
server, ensuring only one request sees a sub-limit count at a time.

Concurrent requests now correctly queue and are rejected once the limit
is reached, preventing quota bypass.

Closes geturbackend#255

Signed-off-by: Anshul Jain <anshul23102@iiitd.ac.in>
@anshul23102

Copy link
Copy Markdown
Contributor Author

Hi @getturbackend team, this PR is ready for review. Could you please add the gssoc-approved label so it counts under GSSoC 2026?

Label suggestions for max GSSoC scoring:

  • gssoc-approved (required for scoring)
  • bug (fixes a quota bypass vulnerability)
  • gssoc : level2 or similar (intermediate: concurrency fix with Lua atomicity)

Thank you!

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93a0432c-0327-47cf-8bd1-1159c8296675

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

[!WARNING]

Walkthrough skipped

File diffs could not be summarized.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Nitin-kumar-yadav1307

Copy link
Copy Markdown
Collaborator

@coderabbitai Can you check if this PR includes a test that fires two simultaneous POST requests at maxExports=1 (per the issue's own repro steps) and asserts only one succeeds and flag it as a merge blocker if missing?

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr view 347 --repo geturbackend/urBackend --json files,title,body -q '.files[].path'
echo "---"
gh pr diff 347 --repo geturbackend/urBackend | head -400

Length of output: 2682


@Nitin-kumar-yadav1307 Confirmed — I checked the PR diff and the only file changed is apps/dashboard-api/src/controllers/dbExport.controller.js. There are no test files included in this PR, so there is no test that fires two simultaneous POST requests at maxExports=1 and asserts only one succeeds.

Given that the entire point of this fix is to close a concurrency bug, and the issue's own repro steps describe exactly that race scenario, I'd flag the missing regression test as a merge blocker. Without it, a future refactor (e.g., someone "simplifying" the Lua script back to get/check/incr) could silently reintroduce the race with nothing to catch it.

Suggested test to add (e.g., in a dbExport.controller.test.js):

it('rejects concurrent requests exceeding maxExports=1', async () => {
  // mock redis.eval to simulate the atomic Lua behavior, or use a real/in-memory redis
  const [res1, res2] = await Promise.all([
    request(app).post('/export').send({ projectId, collectionName, userId, email }),
    request(app).post('/export').send({ projectId, collectionName, userId, email }),
  ]);

  const statuses = [res1.status, res2.status].sort();
  expect(statuses).toEqual([200, 429]); // exactly one succeeds, one is rate-limited
});

If a real Redis instance isn't available in CI, an alternative is a targeted unit test that mocks redis.eval to return [1, 1] for the first call and [0, 1] for the second call fired concurrently, verifying the controller logic branches correctly on both outcomes.

@anshul23102 you may want to add this test before requesting final review — happy to re-check once it's in.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Nitin-kumar-yadav1307

Copy link
Copy Markdown
Collaborator

@anshul23102 logic fix looks solid,but we need a test that actually proves it, not just code that looks atomic

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] dashboard-api: dbExport daily limit check has TOCTOU race — concurrent requests can bypass per-day export cap

2 participants