# Claimed listings show Verified Owner badge set server-side only

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

## Why it matters

A verified owner badge that is set client-side or derived from user-supplied input is cosmetic — any user can fake it by editing the DOM or intercepting the API response. Visitors rely on the badge to distinguish operator-managed listings from third-party submissions. When the badge is fabricated, trust signals collapse and the directory's credibility is undermined. CWE-285 and OWASP A01 (Broken Access Control) apply: authorization state must always originate from the server.

## Severity rationale

High because a client-side or user-controllable badge can be faked by any listing submitter, making the verified owner signal meaningless to visitors.

## Remediation

Render the badge exclusively from server-fetched data. Never pass a `isVerified` prop from client state or URL params — derive it from `claimed_at` in the database row:

```tsx
// app/listings/[id]/page.tsx (server component)
const listing = await db.listings.findUniqueOrThrow({
  where: { id: params.id },
  select: { title: true, description: true, claimed_at: true }
})

return (
  <article>
    <h1>{listing.title}</h1>
    {listing.claimed_at && (
      <span className="badge-verified">Verified Owner</span>
    )}
  </article>
)
```

Never expose a `claimed_at` override via any public-facing API endpoint. The field should only be writable by the claim verification flow in `app/api/listings/[id]/verify-claim/route.ts`.

## Detection

- **ID:** `verified-owner-badge`
- **Severity:** `high`
- **What to look for:** Examine how listings display ownership. If a listing has been claimed, check that: (1) a "Verified Owner" or similar badge is shown, (2) the badge is set server-side based on the `claimed_at` or `owner_id` field, not client-side, (3) the badge cannot be faked by a non-owner.
- **Pass criteria:** Enumerate all relevant code paths. Claimed listings display a server-rendered badge that indicates verified ownership. The badge is present only for listings where `claimed_at` is set and verified. with at least 1 verified instance.
- **Fail criteria:** No badge for claimed listings, or badge is set client-side or based on user input rather than the database.
- **Skip (N/A) when:** The project has no claim feature.
- **Detail on fail:** `"Claimed listings have no badge to show the owner is verified. Users can't tell if a listing is legitimate."` or `"Badge is shown based on client-side data — user can fake it by editing the page."`
- **Remediation:** Render verified badges server-side:

  ```tsx
  // components/ListingCard.tsx
  export function ListingCard({ listing }) {
    return (
      <article>
        <h3>{listing.title}</h3>
        <p>{listing.description}</p>

        {listing.claimed_at && (
          <badge className="verified-owner">
            ✓ Verified Owner
          </badge>
        )}
      </article>
    )
  }

  // app/listings/[id]/page.tsx
  export default async function ListingPage({ params }) {
    const listing = await db.listings.findUnique({
      where: { id: params.id },
      select: {
        title: true,
        description: true,
        claimed_at: true,
        owner_id: true
      }
    })

    return <ListingCard listing={listing} />
  }
  ```

## External references

- cwe CWE-285
- owasp A01

Taxons: content-integrity, access-control

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