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.
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.
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.
ID: directory-submissions-moderation.spam-prevention.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:
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() }
})
}
}