# Images/photos in reviews are moderated or require explicit approval

- **Pattern:** `ab-001215` (`ecommerce-reviews.moderation-trust.review-images-moderated`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001215
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001215)

## Why it matters

Review image uploads with no file type validation, size limits, or approval gate are a CWE-434 (Unrestricted File Upload) and CWE-79 (XSS) exposure: an attacker can upload an SVG with embedded `<script>` tags or a polyglot file that executes when rendered inline. OWASP A04:2021 (Insecure Design) covers the absence of upload constraints at the design level. Even without malicious intent, a user uploading a 50MB RAW photo file will exhaust serverless function memory and trigger a 500 error for all concurrent visitors. Images served directly from an S3 path with no content-type header enforcement allow MIME-type confusion attacks in older browsers.

## Severity rationale

Medium because unvalidated image uploads enable stored XSS via SVG polyglots and file-size denial-of-service without requiring elevated privileges.

## Remediation

Enforce file type and size limits at the upload handler in `api/reviews/submit`, and require admin approval before images are displayed publicly.

```ts
// api/reviews/submit — image validation
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']
const MAX_SIZE_BYTES = 5 * 1024 * 1024 // 5MB

for (const file of uploadedFiles) {
  if (!ALLOWED_TYPES.includes(file.type)) {
    return Response.json({ error: `File type ${file.type} not allowed` }, { status: 400 })
  }
  if (file.size > MAX_SIZE_BYTES) {
    return Response.json({ error: 'Image exceeds 5MB limit' }, { status: 400 })
  }
}

// Store with pending status — never display immediately
await db.reviewImages.create({
  data: { review_id: reviewId, file_path: uploadedPath, status: 'pending' }
})
```

Only query `WHERE status = 'approved'` when rendering images on the product page. Reject SVG and GIF entirely to eliminate the highest-risk polyglot vectors.

## Detection

- **ID:** `review-images-moderated`
- **Severity:** `medium`
- **What to look for:** Check whether the review submission form includes a file upload input (type="file", accept="image/*"). If uploads are supported, enumerate all image processing steps: (1) file type validation, (2) file size limit check (under 5MB recommended), (3) virus/malware scan, (4) explicit content detection, (5) manual approval status. Count how many of these 5 safety layers are present.
- **Pass criteria:** If image uploads are supported, at least 2 of 5 safety layers are implemented before images are displayed publicly (e.g., file type validation + size limit, or approval status + virus scan). Alternatively, the review form does not support image uploads at all (this is also a pass).
- **Fail criteria:** Image uploads are supported but fewer than 2 of 5 safety layers are present — images are displayed immediately after upload with no validation or moderation.
- **Skip (N/A) when:** The review submission form does not include any file upload input and no image upload feature exists in the review flow.
- **Detail on fail:** `"Image upload supported via file input. 0 of 5 safety layers found. Photos are stored and displayed immediately without validation or moderation."`
- **Remediation:** Add image moderation in the review submission handler at `api/reviews/submit` with a separate images table:

  ```ts
  // Option 1: Require approval
  await db.reviewImages.create({
    data: {
      review_id: reviewId,
      file_path: uploadedPath,
      status: 'pending'
    }
  })

  // Only display approved images
  const images = await db.reviewImages.findMany({
    where: { review_id: reviewId, status: 'approved' }
  })

  // Option 2: Use a virus scanning library
  import { scanFile } from 'some-virus-scan-library'
  const result = await scanFile(uploadedFile)
  if (!result.clean) {
    return { error: 'Image failed security scan' }
  }
  ```

---

## External references

- cwe CWE-434
- owasp A04
- cwe CWE-79

Taxons: injection-and-input-trust, content-integrity

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