# Validation schemas are actually parsed at runtime

- **Pattern:** `ab-002613` (`project-snapshot.security.validation-schemas-have-runtime-use`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-23
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002613
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002613)

## Why it matters

The single most common AI-coding-tool security-theater failure: Zod, Yup, Joi, Valibot, or ArkType is added to `package.json`, a `schemas.ts` file is populated with beautiful `z.object({ ... })` definitions, and then nothing ever actually calls `.parse()`, `.safeParse()`, `.validate()`, or `.assert()` at runtime. Route handlers read `await req.json()` and pipe the raw body straight into a database write, an external API call, or an email template. The schema exists only as a type alias via `z.infer<typeof X>`. The code LOOKS validated — reviewers see the schema file, the types line up, the IDE autocompletes — but the wire is wide open. OWASP A03 (Injection) and A04 (Insecure Design) are the direct mappings: without a runtime parse, mass-assignment attacks (sending `{ email, role: "admin" }` to a signup endpoint), prototype pollution via crafted payloads, type-coercion exploits, and stored XSS via unsanitized text all reach your data layer unfiltered. Cursor and v0 produce this anti-pattern by default when asked to "add Zod" — they scaffold the schema and stop there.

## Severity rationale

Critical because dangling schemas provide zero runtime protection — every mutation endpoint that appears validated is actually wide open, and the attack surface spans every OWASP injection and mass-assignment class simultaneously.

## Remediation

Parse every request body through a schema at the route entry point BEFORE any downstream code (database write, external API call, state mutation) reads from it:
  ```ts
  // app/api/users/route.ts
  import { CreateUserSchema } from '@/lib/schemas';

  export async function POST(req: Request) {
    const body = await req.json();
    const data = CreateUserSchema.parse(body); // throws ZodError on invalid input
    const user = await db.user.create({ data });
    return Response.json(user, { status: 201 });
  }
  ```
  For tRPC, use `.input(Schema)` on the procedure. For Server Actions, use `parseWithZod` from `@conform-to/zod` or pass the schema to a `safeAction` helper. Audit every exported schema by grepping its name across the codebase — if the only hits are `z.infer<typeof X>`, the schema is dead code and the corresponding handler is unprotected. For a deeper input-validation sweep including sanitization, rate limits, and allowlist-based field gating, run the `security-hardening` and `api-security` Pro audits.

## Detection

- **ID:** `validation-schemas-have-runtime-use`
- **Severity:** `critical`
- **What to look for:** Check `package.json` for: `zod`, `yup`, `joi`, `valibot`, `arktype`, `superstruct`, `class-validator`, `io-ts`, `runtypes`, `@sinclair/typebox`. If any present, enumerate every POST/PUT/PATCH/DELETE handler under `app/api/**/route.ts`, `pages/api/**`, and Server Actions reading a request body (`await req.json()`, `await req.formData()`, `await request.text()`, `formData.get(...)`, tRPC `input`). For each, check whether the body passes through a validation call BEFORE use downstream: `.parse(...)`, `.safeParse(...)`, `.parseAsync(...)`, `.validate(...)`, `.assert(...)`, `parseWithZod({ formData, schema })`, tRPC `.input(X)`, `zodResolver(X)`.
- **Pass criteria:** Every body-consuming mutation validates before using the body.
- **Fail criteria:** Any handler reads a body and passes it (direct or destructured) to a DB call / external API / state mutation without a validation call. A schema that exists only as `z.infer<typeof X>` TypeScript type hint is NOT validation — this is the canonical AI security-theater pattern and must fail.
- **Skip (N/A) when:** No validation library in deps AND no body-consuming mutation handlers. If library is in deps but no mutation handlers exist, pass (no attack surface). Quote the dep list + route walk.
- **Before evaluating, quote:** `package.json` deps + full source of each flagged handler + its schema import (if any).
- **Report even on pass:** `"Validation library: zod. 18 handlers inspected, all 18 call Schema.parse(await req.json()) before db writes."`
- **Detail on fail:** `"app/api/signup/route.ts reads `const body = await req.json()` and passes body to `db.user.create({ data: body })` — Zod is in deps and `CreateUserSchema` exported from src/lib/schemas.ts but never invoked at runtime; mass-assignment surface open"`.
- **Remediation:**
  ```ts
  import { CreateUserSchema } from '@/lib/schemas';
  export async function POST(req: Request) {
    const body = await req.json();
    const data = CreateUserSchema.parse(body);  // throws on invalid input
    const user = await db.user.create({ data });
    return Response.json(user, { status: 201 });
  }
  ```
  For tRPC: `.input(Schema)`. For Server Actions: `parseWithZod` from `@conform-to/zod`. A schema that is never invoked is just a type hint — the runtime accepts any JSON.

## External references

- cwe CWE-20
- cwe CWE-915
- owasp A03
- owasp A04

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

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