# Review submission prevents duplicate submissions from same user per product

- **Pattern:** `ab-001201` (`ecommerce-reviews.review-collection.duplicate-prevention`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001201
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001201)

## Why it matters

Without a unique constraint on (user_id, product_id), a single user can flood your product with five-star reviews — or a malicious competitor can script hundreds of one-star submissions. CWE-799 (Improper Control of Interaction Frequency) names this exact failure. Beyond rating manipulation, duplicate reviews distort your aggregate score fed to schema.org AggregateRating, poisoning the star display that Google indexes. The fix is two-layer: a database constraint that survives any future code path, plus an application-level check that returns a user-facing error immediately.

## Severity rationale

High because a missing constraint enables unlimited rating manipulation that corrupts product trust signals and schema.org data without requiring any special privileges.

## Remediation

Add a unique constraint in `prisma/schema.prisma` (or a raw SQL migration) and guard the insert in `api/reviews/submit`.

```sql
-- Migration: enforce one review per user per product
ALTER TABLE reviews
  ADD CONSTRAINT unique_user_product UNIQUE (user_id, product_id);
```

```ts
// api/reviews/submit
const existing = await db.reviews.findFirst({
  where: { user_id: userId, product_id: productId }
})
if (existing) {
  return Response.json({ error: 'You have already reviewed this product' }, { status: 409 })
}
```

The database constraint is the hard stop; the application check is the early exit that returns a readable error instead of a constraint violation.

## Detection

- **ID:** `duplicate-prevention`
- **Severity:** `high`
- **What to look for:** Enumerate all duplicate-prevention mechanisms: (1) database unique constraint on (user_id, product_id), (2) application-level check before insert, (3) UI disabling of form for already-reviewed products. Count how many layers of protection exist (minimum 1 required, at least 2 recommended).
- **Pass criteria:** At least 1 duplicate prevention mechanism enforces that a user can submit no more than 1 review per product, either via a database unique constraint on (user_id, product_id) or an application-level existence check before insert, with user-facing error messaging on duplicate attempt.
- **Fail criteria:** No duplicate prevention mechanism exists at any layer — a user can submit multiple reviews for the same product without any block or warning.
- **Skip (N/A) when:** Reviews are anonymous and not tied to user accounts (no user_id column in reviews table or no auth required for review submission).
- **Detail on fail:** `"0 of 3 duplicate-prevention layers found. Database schema has no unique constraint on (user_id, product_id). No application check before insert. No UI disabling for reviewed products."` or `"Form has no check — user can resubmit and create duplicate reviews."`
- **Cross-reference:** Related to `ecommerce-reviews.moderation-trust.moderation-enforced` (moderation catches some duplicates but is not a substitute for prevention).
- **Remediation:** Add a unique constraint to your reviews table in `prisma/schema.prisma` or via SQL migration and check before allowing submission in `api/reviews/submit`:

  ```sql
  -- Database-level constraint
  ALTER TABLE reviews
  ADD CONSTRAINT unique_user_product UNIQUE (user_id, product_id);
  ```

  At application level, check before allowing submission:

  ```ts
  // api/reviews/submit
  const existing = await db.reviews.findFirst({
    where: { user_id: userId, product_id: productId }
  })

  if (existing) {
    return { error: 'You have already reviewed this product' }
  }
  ```

## External references

- cwe CWE-799

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001201
