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.
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.
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.
project-snapshot.security.validation-schemas-have-runtime-usecriticalpackage.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).z.infer<typeof X> TypeScript type hint is NOT validation — this is the canonical AI security-theater pattern and must fail.package.json deps + full source of each flagged handler + its schema import (if any)."Validation library: zod. 18 handlers inspected, all 18 call Schema.parse(await req.json()) before db writes.""app/api/signup/route.ts reads const body = await req.json()and passes body todb.user.create({ data: body })— Zod is in deps andCreateUserSchema exported from src/lib/schemas.ts but never invoked at runtime; mass-assignment surface open".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.