Skip to content

fix: address 4 critical issues in promo code endpoint#25

Closed
Njko wants to merge 1 commit into
DevExpGbb:mainfrom
Njko:fix/promo-code-critical-issues
Closed

fix: address 4 critical issues in promo code endpoint#25
Njko wants to merge 1 commit into
DevExpGbb:mainfrom
Njko:fix/promo-code-critical-issues

Conversation

@Njko

@Njko Njko commented Jun 22, 2026

Copy link
Copy Markdown

Fixes for PR #24

This PR addresses 4 critical issues identified in the code review of PR #24 (promo-code endpoint):

1. Race Condition (app/api/carts/[cartId]/apply-promo/route.ts:102-107)

Problem: Cart subtotal can be modified by another request between read and update
Solution: Added optimistic locking using updated_at timestamp
Impact: Ensures discount is calculated against consistent data

Changes:

  • Modified updateCartWithPromo to accept previousUpdatedAt parameter
  • Added WHERE clause: AND updated_at = $6 to check optimistic lock
  • Returns boolean to indicate success/conflict

2. Uncaught Zod Validation Error (app/api/carts/[cartId]/apply-promo/route.ts:110-119)

Problem: Uses .parse() instead of .safeParse(), masks validation failures as 500 errors
Solution: Changed to .safeParse() with explicit error handling
Impact: Proper error messages for debugging and client feedback

Changes:

  • Response validation now uses .safeParse()
  • Added explicit validation error handling
  • Returns 500 with proper logging on validation failure

3. Silent Failure on Cart Update (lib/db.ts:76-83)

Problem: UPDATE doesn't check rowCount, so cart deletion between validation and update goes undetected
Solution: Check result.rowCount === 1 before returning
Impact: Prevents returning success when cart was deleted

Changes:

  • updateCartWithPromo now returns Promise<boolean>
  • Returns result.rowCount === 1 to verify exactly 1 row was updated
  • Route handler checks return value and returns 409 Conflict on failure

4. Information Disclosure (app/api/carts/[cartId]/apply-promo/route.ts:73, 85, 96)

Problem: Error messages expose cartId and promo code existence
Solution: Use generic error messages that don't reveal internal structure
Impact: Reduces attack surface

Changes:

  • Line 73: Removed Cart with ID ${cartId} from error message
  • Line 85: Removed Promo code "${promoCode}" does not exist
  • Line 96: Removed Promo code "${promoCode}" is expired or inactive
  • All replaced with generic: "Promo code is invalid"

Verification

✅ All 25 tests still pass
✅ TypeScript compilation succeeds with no errors
✅ All good parts of original implementation preserved
✅ Changes are minimal and surgical

Files Modified

  • app/api/carts/[cartId]/apply-promo/route.ts - Add optimistic locking, use safeParse, generic errors
  • lib/db.ts - Check rowCount, return boolean for update status

…error handling, information disclosure)

- Add optimistic locking to prevent race condition on cart update
- Use safeParse() for response validation instead of parse()
- Check rowCount to detect silent failures on cart update
- Replace information-disclosing error messages with generic ones

Fixes race condition where cart subtotal could be modified between read and
update. Uses optimistic locking with updated_at timestamp to ensure consistency.

Changes response validation to use safeParse() and handles validation errors
explicitly, preventing validation errors from being masked as 500 errors.

Adds rowCount check in updateCartWithPromo to detect when cart was deleted
between validation and update, preventing silent failures.

Replaces error messages that expose cartId and promo code existence with
generic error messages to reduce attack surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Njko

Njko commented Jun 22, 2026

Copy link
Copy Markdown
Author

Closing this PR to address critical security and architecture issues. Will resubmit on fork with comprehensive fixes.

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.

1 participant