Skip to content

fix: atomic capped increment for promo code redemption - #106

Closed
0xzino wants to merge 2 commits into
profullstack:masterfrom
0xzino:fix/promo-race-atomic-increment
Closed

fix: atomic capped increment for promo code redemption#106
0xzino wants to merge 2 commits into
profullstack:masterfrom
0xzino:fix/promo-race-atomic-increment

Conversation

@0xzino

@0xzino 0xzino commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Bug

quotePromo() validates uses < max_uses, then recordPromoRedemption() performs a blind UPDATE promo_codes SET uses = uses + 1. Under concurrency, two requests can both pass the quote check and both increment, over-redeeming the code (issue #92, race condition).

Fix

Make the increment atomic and capped:

UPDATE promo_codes SET uses = uses + 1
WHERE code = ? AND (max_uses IS NULL OR uses < max_uses)

The WHERE guard makes over-redemption impossible: SQLite serializes writes, so once uses reaches max_uses, the UPDATE matches zero rows.

Verification

Simulated 5 concurrent users all passing the quote check against a max_uses = 2 code: final uses = 2 (capped), no over-redemption.

Closes #92

0xzino added 2 commits July 31, 2026 04:03
SESSION_SECRET fell back to a hardcoded dev value even in production,
making session token hashes trivially forgeable (issue profullstack#88, HIGH).

Now sessionSecret is a getter that throws if SESSION_SECRET is unset
when nodeEnv === 'production', while keeping the dev fallback for
local development.

Verified: prod+unset throws, prod+set returns secret, dev+unset returns
fallback.
quotePromo() checks uses < max_uses, then recordPromoRedemption() did a
blind 'uses = uses + 1'. Two concurrent requests both pass the quote
check and over-redeem the code (profullstack#92).

Now the increment is an atomic UPDATE guarded by
(uses < max_uses OR max_uses IS NULL), so uses can never exceed the cap
even under concurrency. Verified with a 5-way concurrent simulation:
5 quotes pass, uses stays capped at max_uses=2.
@ralyodio

ralyodio commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Closing as superseded by #110, now merged.

#110 covers the same capped increment (WHERE code = ? AND (max_uses IS NULL OR uses < max_uses)) and additionally makes the per-user duplicate guard atomic via INSERT OR IGNORE + a rowsAffected check, which is the actual TOCTOU in #92 — the capped increment alone bounds the counter but still lets a second concurrent redemption be granted.

The env.ts SESSION_SECRET hunk here had already landed separately in #103, so master has it too. This branch now conflicts. Thanks for the fix.

@ralyodio ralyodio closed this Aug 1, 2026
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.

MEDIUM: Race condition in promo code duplicate redemption check

2 participants