# Listing owners can self-flag a listing as closed without moderator action

- **Pattern:** `ab-001092` (`directory-submissions-moderation.spam-prevention.self-flag-closed`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001092
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001092)

## Why it matters

When listing owners cannot close their own listings without contacting support, closed or defunct businesses remain in the directory indefinitely — polluting search results, misleading visitors, and generating dead-end clicks. GDPR Art. 17 (right to erasure) creates an obligation to honor removal requests promptly; routing that through a support ticket introduces unnecessary delay. CWE-285 applies when the authorization model lacks a scope the owner clearly should have: control over their own content.

## Severity rationale

Low because the failure is primarily a UX and data-quality gap — stale listings mislead users — rather than a direct security vulnerability.

## Remediation

Add a close action to the owner dashboard that sets `status = 'closed'` and soft-hides the listing. Never hard-delete: keep the row for audit history and allow the owner to reopen later:

```ts
// app/api/listings/[id]/close/route.ts
export async function POST(req: Request) {
  const user = await requireAuth(req)
  const listing = await db.listings.findUniqueOrThrow({ where: { id: params.listingId } })
  if (listing.owner_id !== user.id) {
    return Response.json({ error: 'Unauthorized' }, { status: 403 })
  }
  await db.listings.update({
    where: { id: listing.id },
    data: { status: 'closed', closed_at: new Date() }
  })
  return Response.json({ message: 'Listing closed' })
}
```

In `app/dashboard/listings/[id]/page.tsx`, surface a "Close Listing" button that calls this route. Filter `status = 'closed'` from all public listing queries the same way you filter `'pending'` and `'rejected'`.

## Detection

- **ID:** `self-flag-closed`
- **Severity:** `low`
- **What to look for:** Check if listing owners have a simple way to mark their own listing as closed/inactive without requesting moderator intervention. Look for an owner dashboard or account page with a "Close Listing" or "Mark as Closed" button.
- **Pass criteria:** Enumerate all relevant code paths. Owners can directly close/archive their own listings. The listing is hidden from the public directory but remains in the database. Owners can reopen the listing later. with at least 1 verified instance.
- **Fail criteria:** Owners cannot close their own listings without contacting support, or closing a listing hard-deletes it.
- **Skip (N/A) when:** The project doesn't have an owner/account dashboard.
- **Detail on fail:** `"Owners can't close their own listings. A business that's shutting down has to contact support to remove their listing."` or `"Listings are hard-deleted on closure — no way to archive them."`
- **Remediation:** Add a self-close feature:

  ```tsx
  // app/dashboard/listings/[id]/page.tsx
  export default async function ListingSettings({ params }) {
    const listing = await db.listings.findUnique({
      where: { id: params.id, owner_id: req.auth.userId }
    })

    return (
      <div>
        <h1>{listing.title}</h1>
        <button
          onClick={() => fetch(`/api/listings/${listing.id}/close`, { method: 'POST' })}
        >
          Close This Listing
        </button>
      </div>
    )
  }

  // app/api/listings/[id]/close/route.ts
  export async function POST(req: Request) {
    const { listingId } = params
    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 })
    }

    await db.listings.update({
      where: { id: listingId },
      data: { status: 'closed', closed_at: new Date() }
    })

    return Response.json({ message: 'Listing closed' })
  }
  ```

---

---

## External references

- cwe CWE-285
- gdpr Art. 17

Taxons: user-experience, access-control

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