Client-side validation is advisory — any attacker with curl can bypass it entirely. Without server-side enforcement on every field, a crafted POST with a missing title or malformed email doesn't get rejected at 400; it either crashes the handler with a 500 (leaking stack traces) or silently inserts garbage into your database. OWASP A03 (Injection) and CWE-20 (Improper Input Validation) both call this out as a critical failure mode: the server is the only trust boundary that matters.
Critical because client-side bypasses are trivial and unvalidated inputs can cause 500 errors, crash handlers, or corrupt the database directly.
Add a Zod schema in your API route and call .parse() before touching the database. Any ZodError returns a structured 400; all other errors stay as 500. In app/api/listings/submit/route.ts:
import { z } from 'zod'
const submissionSchema = z.object({
title: z.string().min(10).max(200),
description: z.string().min(50).max(5000),
contactEmail: z.string().email(),
category: z.string().min(1)
})
export async function POST(req: Request) {
const body = await req.json()
const result = submissionSchema.safeParse(body)
if (!result.success) {
return Response.json({ error: 'Validation failed', issues: result.error.issues }, { status: 400 })
}
// proceed with result.data
}
ID: directory-submissions-moderation.submission-form.server-validation
Severity: critical
What to look for: Examine the API route that handles form submission (e.g., POST /api/listings/submit). Check that validation is performed on all required and constrained fields (title length, email format, etc.). Test what happens when you send a crafted request with missing or invalid data — the response should be a 400 (Bad Request) with a clear error message, not a 500 (Internal Server Error).
Pass criteria: Enumerate all relevant code paths. Server validates all fields against the same rules as the client. Invalid requests return 400 with structured error messages. No server 500 errors occur due to data validation failures. Quote the actual validation rules or configuration used.
Fail criteria: No server-side validation, or validation only partially covers fields, or invalid requests return 500 errors, or validation is incomplete (e.g., allows email-like strings that aren't valid). A partial implementation does not count as pass.
Skip (N/A) when: The project has no submission API or uses a third-party service.
Detail on fail: "Server accepts submissions with missing title field without validation. POST /api/listings returns 500 when required fields are missing." or "Client validates but server does not — server accepts invalid email formats."
Cross-reference: Compare with directory-submissions-moderation.submission-form.client-validation — server-side validation is the enforcement layer that client-side cannot replace.
Remediation: Implement server-side validation on all form endpoints:
// app/api/listings/submit/route.ts
import { z } from 'zod'
const submissionSchema = z.object({
title: z.string().min(10, 'Title must be at least 10 characters').max(200),
description: z.string().min(50, 'Description must be at least 50 characters').max(5000),
contactEmail: z.string().email('Invalid email address'),
phone: z.string().regex(/^\+?[0-9\s\-()]+$/, 'Invalid phone format').optional(),
category: z.string().min(1)
})
export async function POST(req: Request) {
try {
const body = await req.json()
const parsed = submissionSchema.parse(body)
// Proceed with submission
const submission = await db.listings.create({ data: parsed })
return Response.json({ id: submission.id }, { status: 201 })
} catch (err) {
if (err instanceof z.ZodError) {
return Response.json(
{ error: 'Validation failed', issues: err.issues },
{ status: 400 }
)
}
console.error('Submission error:', err)
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}