Owners edit published listings via pending edit queue, not direct overwrite
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:
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_queueorpending_editstable. -
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:
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 );// 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 — Improper Authorization
- owasp:2021 · A01 — Broken Access Control
Taxons
History
- 2026-04-18·v1.0.0·Initial import from directory-submissions-moderation·automated