# User input is validated server-side, not just client-side

- **Pattern:** `ab-002574` (`project-snapshot.security.input-validated-server-side`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-22
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002574
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002574)

## Why it matters

A route handler that does `const body = await request.json()` and passes the result straight to Prisma, Supabase, or a raw SQL call is trusting whatever the client sends — including type mismatches, unexpected fields, oversized payloads, and deliberately malformed structures. OWASP 2021 A03 Injection remains the canonical cause of most mega-breaches, and client-side-only validation means an attacker who pipes to your API via curl bypasses every check you wrote in the browser. Without a schema parse in front, attackers can submit extra fields that the ORM happily writes (mass-assignment), coerce types to trigger errors that leak stack traces, or send payloads large enough to DoS the JSON parser. AI coding tools consistently skip this step — they'll generate a handler that destructures the request body, use the fields immediately, and move on, because the happy path works and validation adds visible line count. Zod / Yup / Valibot / Pydantic aren't hard to add; they just get forgotten.

## Severity rationale

High when any database-writing handler skips validation because mass-assignment and type-coercion bugs follow directly; elevated above medium because a single unvalidated write endpoint is typically enough to compromise data integrity.

## Remediation

Define a Zod schema and parse at the top of every handler:
  ```ts
  const Body = z.object({ email: z.string().email(), name: z.string().min(1).max(120) })
  const body = Body.parse(await request.json())
  // use body.email, body.name
  ```

Deeper remediation guidance and cross-reference coverage for this check lives in the `security-hardening` Pro audit — run that after applying this fix for a more exhaustive pass on the same topic.

## Detection

- **ID:** `input-validated-server-side`
- **Severity:** `high`
- **What to look for:** Enumerate every API route handler / route handler / server action / form action in the project. For each, check whether the request body, query params, or form data are parsed **server-side** through a recognized validation library — Zod, Yup, Joi, Valibot, ArkType, class-validator, Ajv, Pydantic, marshmallow — called INSIDE the route handler (not just imported in client components), OR manual parsing with explicit per-field checks on type, length, enum membership, and whitelist rules. Client-side HTML5 attributes (`required`, `maxlength`, `pattern=""`, `type="email"`) do NOT count as validation since any attacker can bypass them by hitting the API directly with curl. Count handlers with server-side validation vs. handlers without.
- **Pass criteria:** At least 80% of handlers that consume request data validate it **server-side** through a recognized library before use, AND no handler that writes to a database uses unvalidated input.
- **Fail criteria:** Less than 80% server-side validation rate, OR any database-writing handler skips validation, OR validation is only present as HTML5 attributes on the client-side form with nothing gating the route.
- **Skip (N/A) when:** No API routes / server actions / route handlers detected (project is purely static).
- **Do NOT pass when:** Validation exists but runs AFTER the database write — the order matters. Also do not pass when only some fields are validated and the rest pass through as-is. Also do not pass when "validation" means only client-side HTML5 form attributes (`type="email"`, `required`, `pattern`) with no server-side check — an attacker who pipes to your API via curl bypasses every one of those.
- **Report even on pass:** `"Found N handlers that consume request data; M validate via {library list}. Validation rate: M/N = X%."`
- **Detail on fail:** `"4 of 12 API handlers parse request body without Zod/Yup validation; 1 of those writes to the database (app/api/comments/route.ts)"`.
- **Remediation:** Define a Zod schema and parse at the top of every handler:
  ```ts
  const Body = z.object({ email: z.string().email(), name: z.string().min(1).max(120) })
  const body = Body.parse(await request.json())
  // use body.email, body.name
  ```

Taxons: project-snapshot, injection-and-input-trust

HTML version: https://auditbuffet.com/patterns/ab-002574
