An unprotected submission form is an automated spam target. Bots discover open endpoints within hours of launch and flood moderation queues with hundreds of fake listings per minute. Without CAPTCHA, a honeypot field, or server-side rate limiting, your moderators are overwhelmed, storage costs spike, and legitimate submissions get buried. CWE-799 (Improper Control of Interaction Frequency) and OWASP A07 (Identification & Authentication Failures) both apply — the server cannot distinguish human from bot because it never asks.
Critical because an unguarded form enables unlimited automated spam that overwhelms moderation queues and degrades directory quality for every real user.
Add at least one server-validated anti-spam layer. Honeypot is zero-friction for real users; CAPTCHA provides stronger bot detection. For a honeypot in a Next.js form:
{/* Hidden from real users via CSS, filled by bots */}
<input name="website_url" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
// Server: reject if honeypot field is populated
const { website_url, ...data } = await req.json()
if (website_url) {
return Response.json({ error: 'Submission rejected' }, { status: 400 })
}
For higher-volume directories, pair this with IP-based rate limiting (e.g., max 5 submissions per 60 seconds via Upstash Redis) so bot farms that rotate IPs still hit friction.
ID: directory-submissions-moderation.submission-form.spam-prevention-form
Severity: critical
What to look for: Examine the submission form. Look for: (1) a CAPTCHA widget (reCAPTCHA, hCaptcha, Turnstile, etc.), (2) a honeypot field (hidden form field that bots fill in), or (3) server-side rate limiting that blocks rapid submissions from the same IP or user. Check package.json for captcha libraries and the form component for honeypot implementation.
Pass criteria: Enumerate all relevant code paths. At least one of the following is present: (1) CAPTCHA widget shown and validated server-side, (2) honeypot field present and checked server-side, or (3) rate limiting enforced (e.g., max 5 submissions per 60 seconds per IP). with at least 1 verified instance.
Fail criteria: No CAPTCHA, honeypot, or rate limiting on the submission form. A partial implementation does not count as pass.
Skip (N/A) when: The project uses a third-party service that handles form anti-spam.
Detail on fail: "Form has no CAPTCHA, honeypot, or rate limiting. Anyone can submit spam listings repeatedly without any friction." or "CAPTCHA widget visible but server doesn't validate the token — verification is skipped."
Remediation: Add CAPTCHA or honeypot protection:
// Using reCAPTCHA v3
import { useGoogleReCaptcha } from '@react-google-recaptcha-v3'
export function SubmissionForm() {
const { executeRecaptcha } = useGoogleReCaptcha()
const onSubmit = async (data) => {
const token = await executeRecaptcha('submit_listing')
// Send token with form data
await fetch('/api/listings/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, recaptcha_token: token })
})
}
return <form onSubmit={handleSubmit(onSubmit)}>...</form>
}
// Server-side validation
import axios from 'axios'
const secret = process.env.RECAPTCHA_SECRET_KEY
export async function POST(req: Request) {
const { recaptcha_token, ...data } = await req.json()
// Verify CAPTCHA
const { data: captchaResult } = await axios.post(
`https://www.google.com/recaptcha/api/siteverify`,
{ secret, response: recaptcha_token }
)
if (captchaResult.score < 0.5) {
return Response.json(
{ error: 'CAPTCHA verification failed' },
{ status: 400 }
)
}
// Proceed with submission
}
Or honeypot approach:
<form>
{/* Visible fields */}
<input name="title" />
{/* Honeypot: hidden from users but filled by bots */}
<input
name="website"
style={{ display: 'none' }}
tabIndex={-1}
autoComplete="off"
/>
</form>
// Server: reject if honeypot is filled
const { website, ...data } = await req.json()
if (website) {
return Response.json(
{ error: 'Suspicious submission' },
{ status: 400 }
)
}