Validation schemas are actually parsed at runtime
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:
// 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.jsonfor:zod,yup,joi,valibot,arktype,superstruct,class-validator,io-ts,runtypes,@sinclair/typebox. If any present, enumerate every POST/PUT/PATCH/DELETE handler underapp/api/**/route.ts,pages/api/**, and Server Actions reading a request body (await req.json(),await req.formData(),await request.text(),formData.get(...), tRPCinput). 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.jsondeps + 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 readsconst body = await req.json()and passes body todb.user.create({ data: body })— Zod is in deps andCreateUserSchemaexported from src/lib/schemas.ts but never invoked at runtime; mass-assignment surface open". - Remediation:
For tRPC: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 }); }.input(Schema). For Server Actions:parseWithZodfrom@conform-to/zod. A schema that is never invoked is just a type hint — the runtime accepts any JSON.
External references
- cwe · CWE-20 — Improper Input Validation
- cwe · CWE-915 — Improperly Controlled Modification of Dynamically-Determined Object Attributes
- owasp:2021 · A03 — Injection
- owasp:2021 · A04 — Insecure Design
Taxons
History
- 2026-04-23·v1.0.0·Initial Phase 9.1 v3.1 Stack Scan promotion — dangling validation schemas are the canonical AI security-theater pattern; handlers bypass them routinely.·by phase-9-1-stack-scan-v3-1