# Owners edit published listings via pending edit queue, not direct overwrite

- **Pattern:** `ab-001088` (`directory-submissions-moderation.spam-prevention.edit-approval-queue`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001088
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001088)

## Why it matters

Allowing listing owners to directly overwrite approved content gives any compromised or malicious owner account the ability to replace a legitimate listing with spam, phishing links, or defamatory text that goes live instantly. CWE-285 (Improper Authorization) and OWASP A01 apply — ownership of a listing does not grant the right to bypass editorial review. This is especially acute because a single bad edit can damage the directory's SEO and brand before any moderator notices.

## Severity rationale

High because direct-overwrite edits allow a malicious or hijacked owner account to publish harmful content to a live, indexed listing with zero moderation delay.

## Remediation

Store owner edits in a `listing_edits` staging table and apply them only after moderator approval. The public listing record stays unchanged until the edit is approved:

```sql
CREATE TABLE listing_edits (
  id SERIAL PRIMARY KEY,
  listing_id INT NOT NULL REFERENCES listings(id),
  owner_id VARCHAR NOT NULL,
  field_name VARCHAR(50) NOT NULL,
  old_value TEXT,
  new_value TEXT NOT NULL,
  status VARCHAR NOT NULL DEFAULT 'pending',
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  reviewed_at TIMESTAMPTZ
);
```

In `app/api/listings/[id]/edit/route.ts`, insert rows into `listing_edits` and return `202 Accepted` — never `PATCH` the `listings` row directly. In the admin route, apply approved edits by copying `new_value` back to the parent listing and setting `reviewed_at`.

## Detection

- **ID:** `edit-approval-queue`
- **Severity:** `high`
- **What to look for:** Examine the listing edit flow. Check that when an owner edits a published (approved) listing, the changes: (1) do not immediately update the public version, (2) enter a moderation queue as pending edits, (3) are approved by a moderator before taking effect. Look for an `edit_queue` or `pending_edits` table.
- **Pass criteria:** Enumerate all relevant code paths. Edits to approved listings go through a review process. Changes are not visible to the public until approved. A moderator can review and approve/reject the pending edits. with at least 1 verified instance.
- **Fail criteria:** Edits directly overwrite the published listing without review, or edits are immediately visible.
- **Skip (N/A) when:** The project doesn't allow edits to published listings, or owners cannot edit their own listings.
- **Detail on fail:** `"Owners can directly edit their listings and changes show immediately. A malicious owner could completely change the business description or add spam links."` or `"Edit queue exists but changes are also pushed live immediately."`
- **Remediation:** Implement an edit approval queue:

  ```sql
  CREATE TABLE listing_edits (
    id SERIAL PRIMARY KEY,
    listing_id INT NOT NULL REFERENCES listings(id),
    owner_id VARCHAR NOT NULL,
    field_name VARCHAR(50),
    old_value TEXT,
    new_value TEXT,
    status VARCHAR DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT NOW(),
    approved_at TIMESTAMP
  );
  ```

  ```ts
  // app/api/listings/[id]/edit/route.ts
  export async function POST(req: Request) {
    const { listingId } = params
    const { title, description } = await req.json()
    const ownerId = req.auth.userId

    const listing = await db.listings.findUnique({ where: { id: listingId } })

    if (listing.owner_id !== ownerId) {
      return Response.json({ error: 'Unauthorized' }, { status: 403 })
    }

    // Create pending edits
    if (title !== listing.title) {
      await db.listingEdits.create({
        data: {
          listing_id: listingId,
          owner_id: ownerId,
          field_name: 'title',
          old_value: listing.title,
          new_value: title,
          status: 'pending'
        }
      })
    }

    // Similar for other fields...

    return Response.json({ message: 'Changes submitted for review' })
  }

  // Admin endpoint to approve/reject edits
  export async function POST(req: Request) {
    const { editId, action } = await req.json()

    if (action === 'approve') {
      const edit = await db.listingEdits.findUnique({ where: { id: editId } })

      await db.listings.update({
        where: { id: edit.listing_id },
        data: { [edit.field_name]: edit.new_value }
      })

      await db.listingEdits.update({
        where: { id: editId },
        data: { status: 'approved', approved_at: new Date() }
      })
    }
  }
  ```

## External references

- cwe CWE-285
- owasp A01

Taxons: content-integrity, access-control

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