Client-side validation is a UX feature, not a security control — any attacker can submit arbitrary payloads directly to your API bypassing browser validation entirely. CWE-20 (Improper Input Validation) and OWASP A03 (Injection) both trace back to trusting unvalidated input on the server. NIST 800-53 SI-10 requires server-side input validation on all information system boundaries. Unvalidated inputs enable SQL injection, prototype pollution, type coercion bugs, and business logic bypasses. A quantity: -1 or role: 'admin' in a JSON body should never reach your business logic.
Medium because missing server-side validation enables a wide class of injection and business logic attacks that client-side checks cannot block.
Apply Zod schema validation at the entry point of every API route before executing any business logic:
import { z } from 'zod'
const OrderSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100),
})
export async function POST(req: Request) {
const body = await req.json().catch(() => null)
const result = OrderSchema.safeParse(body)
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 }
)
}
// result.data is now type-safe
}
Apply this pattern consistently across all routes in src/app/api/. Server Actions that accept FormData also require validation — formData.get('quantity') returns string | null and must be parsed before use.
ID: security-hardening.input-validation.server-side-validation
Severity: medium
What to look for: Count every API endpoint that accepts user input. For each, check whether server-side validation (Zod, Joi, class-validator, or manual) is applied before processing. check API route handlers and server actions for schema validation. Look for Zod, Joi, yup, valibot, or manual validation applied to request bodies, query parameters, and path parameters before any business logic executes. Specifically verify that client-side validation is not the only guard — the server must validate independently.
Pass criteria: All API endpoints that accept user input validate it server-side against a defined schema before processing. Invalid requests return 400/422 with error details — 100% of endpoints must validate inputs server-side. Report: "X input-accepting endpoints found, all Y have server-side validation."
Fail criteria: Endpoints process user input with no server-side validation. Or server-side validation is present for some endpoints but inconsistently applied.
Skip (N/A) when: The application has no endpoints that accept user input (read-only APIs serving only authenticated data with no input).
Cross-reference: The sql-injection check verifies query-level protections after validation.
Detail on fail: Identify specific unvalidated endpoints. Example: "POST /api/orders processes req.body without any validation — arbitrary fields accepted" or "Query parameters in /api/search used directly in database query without type validation or bounds checking"
Remediation: Apply schema validation consistently with Zod:
import { z } from 'zod'
const CreateOrderSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100),
shippingAddress: z.object({
street: z.string().max(200),
city: z.string().max(100),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
}),
})
export async function POST(req: Request) {
const body = await req.json().catch(() => null)
const result = CreateOrderSchema.safeParse(body)
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 }
)
}
const { productId, quantity, shippingAddress } = result.data
// Now safe to use
}