Keyword stuffing, excessive link insertion, and spam phrases in listing descriptions are direct attacks on your directory's search ranking and user trust. If they reach the public index, search engines penalize your domain; if they reach users, they damage the directory's reputation as a quality resource. CWE-79 (XSS) applies when unscanned descriptions contain injected script tags. OWASP A03 covers the broader input trust failure. Automated detection gates the moderation queue so human reviewers see pre-filtered content.
Medium because spam descriptions degrade directory SEO and user trust, but the direct security impact depends on whether XSS payloads are also filtered downstream.
Scan descriptions for keyword stuffing, excessive links, and known spam phrases before queuing. Flag — do not auto-reject — suspicious submissions so human review remains the final gate:
// lib/spam-detector.ts
export function detectSpam(text: string): { flagged: boolean; signals: string[] } {
const signals: string[] = []
const words = text.toLowerCase().split(/\s+/)
const freq = words.reduce<Record<string, number>>((acc, w) => {
if (w.length > 3) acc[w] = (acc[w] ?? 0) + 1
return acc
}, {})
for (const [word, count] of Object.entries(freq)) {
if (count >= 5) signals.push(`keyword-stuffed: "${word}" ×${count}`)
}
const links = (text.match(/https?:\/\/\S+/g) ?? []).length
if (links > 5) signals.push(`excessive-links: ${links}`)
for (const phrase of ['buy now', 'click here', 'cheap', 'act now']) {
const n = (text.match(new RegExp(phrase, 'gi')) ?? []).length
if (n >= 3) signals.push(`spam-phrase: "${phrase}" ×${n}`)
}
return { flagged: signals.length > 0, signals }
}
Record spam_signals as a JSONB column on the listing row so moderators can see exactly what triggered the flag.
ID: directory-submissions-moderation.spam-prevention.seo-spam-detection
Severity: medium
What to look for: Examine the submission logic. Check for automated detection of common SEO spam patterns in the description: keyword stuffing (repeated keywords), excessive links, phrases like "click here", "cheap", "buy now" repeated many times. Look for a spam detection library or custom pattern matching.
Pass criteria: Enumerate all relevant code paths. Descriptions are scanned for spam patterns. Suspicious submissions are flagged in the moderation queue for manual review (marked with a spam flag). with at least 1 verified instance.
Fail criteria: No spam scanning, or spam detection exists but doesn't flag suspicious content for review.
Skip (N/A) when: The project doesn't accept free-form descriptions or doesn't scan for spam.
Detail on fail: "Spam descriptions get through without any checks. A user submits 'buy cheap watches buy cheap watches buy cheap watches' and it goes straight to the queue." or "Spam pattern library integrated but results are not used to flag submissions."
Remediation: Scan descriptions for spam patterns:
// app/api/listings/submit/route.ts
function detectSpamPatterns(text: string): { isSuspicious: boolean; patterns: string[] } {
const patterns = []
// Keyword stuffing: same word repeated 5+ times
const words = text.toLowerCase().split(/\s+/)
const wordCounts = {}
words.forEach(word => {
if (word.length > 3) {
wordCounts[word] = (wordCounts[word] || 0) + 1
}
})
Object.entries(wordCounts).forEach(([word, count]) => {
if (count >= 5) {
patterns.push(`Keyword stuffing: "${word}" repeated ${count} times`)
}
})
// Excessive links
const linkCount = (text.match(/https?:\/\/\S+/g) || []).length
if (linkCount > 5) {
patterns.push(`Excessive links: ${linkCount} links found`)
}
// Spam phrases
const spamPhrases = ['click here', 'buy now', 'limited time', 'act now', 'cheap']
spamPhrases.forEach(phrase => {
const count = (text.match(new RegExp(phrase, 'gi')) || []).length
if (count >= 3) {
patterns.push(`Spam phrase: "${phrase}" repeated ${count} times`)
}
})
return {
isSuspicious: patterns.length > 0,
patterns
}
}
export async function POST(req: Request) {
const { description, ...data } = await req.json()
const { isSuspicious, patterns } = detectSpamPatterns(description)
const listing = await db.listings.create({
data: {
...data,
description,
status: isSuspicious ? 'pending-spam-review' : 'pending',
spam_flags: isSuspicious ? patterns : []
}
})
return Response.json({ id: listing.id }, { status: 201 })
}