# Validation schemas have at least one runtime use

- **Pattern:** `ab-000275` (`ai-slop-security-theater.unenforced-validation.validation-schemas-have-runtime-use`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000275
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000275)

## Why it matters

Declaring a Zod or Joi schema and never calling `.parse()` on it is the canonical AI security theater move — the schema exists only as a type alias, and your API accepts arbitrary JSON at runtime. OWASP A03 (Injection) and A04 (Insecure Design) are the direct mappings: without a parse call, mass-assignment attacks, prototype pollution via crafted payloads, and type coercion exploits all reach your database unchecked. If a breach occurs and an auditor finds schemas that were never enforced, the liability argument becomes 'you knew the controls were needed but shipped them as decoration.'

## Severity rationale

Critical because a dangling schema provides zero runtime protection — every route that trusts the schema is unvalidated in production, exposing the full attack surface of OWASP A03 Injection.

## Remediation

Wire each exported schema into a `.parse()` or `.safeParse()` call at the route entry point. The schema and the parse call must be in the same request path — a type declaration elsewhere doesn't count.

```ts
// src/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 bad input
  const user = await prisma.user.create({ data })
  return Response.json(user, { status: 201 })
}
```

For tRPC routes, use `.input(Schema)` on the procedure. For Server Actions, use `parseWithZod` or `safeAction`. Search for every exported schema name in your source tree and confirm at least one call site exists.

## Detection

- **ID:** `validation-schemas-have-runtime-use`
- **Severity:** `critical`
- **What to look for:** When any validation library is in `package.json` `dependencies` (one of: `zod`, `yup`, `joi`, `valibot`, `superstruct`, `class-validator`, `io-ts`, `runtypes`, `arktype`, `@sinclair/typebox`), walk all source files and count all exported schema declarations: `export const X = z.object(...)`, `export const X = z.array(...)`, `Joi.object(...)`, `yup.object(...)`, `valibot.object(...)`. For each exported schema name `X`, before evaluating, extract and quote the schema declaration line. Then search the entire `src/`, `app/`, `lib/`, `server/`, `pages/`, `api/` tree (excluding test directories) for at least 1 runtime call site referencing the schema by name: `X.parse(`, `X.safeParse(`, `X.parseAsync(`, `X.safeParseAsync(`, `X.validate(`, `X.validateSync(`, `X.assert(`, `parse(X, ...)`, `validateRequest(X, ...)`, `useForm({ resolver: zodResolver(X) })`, `useForm({ resolver: yupResolver(X) })`, `useForm({ resolver: valibotResolver(X) })`, `tRPCRouter.input(X)`, `safeAction(X)`, `useFormState(action, X)`, `parseWithZod({ schema: X })`. Count: total exported schemas, total with at least 1 runtime use, total dangling. EXCLUDE schemas whose only use is type inference (`type T = z.infer<typeof X>` and similar).
- **Pass criteria:** 100% of exported validation schemas have at least 1 runtime call site. Report: "Validation library: [name]. X exported schemas inspected, Y with runtime use, 0 dangling."
- **Fail criteria:** At least 1 exported schema is declared but never invoked at runtime — defined for type inference only.
- **Do NOT pass when:** A schema is only used inside `z.infer<typeof Y>` and has zero `.parse()`/`.safeParse()`/resolver calls anywhere in the codebase. The canonical AI security theater pattern.
- **Skip (N/A) when:** No validation library is in dependencies.
- **Report even on pass:** Always report the validation library name and the count of schemas inspected. Example: "Validation library: zod. 14 exported schemas inspected, 14 with runtime use (100%)."
- **Cross-reference:** For deeper input validation analysis, the Security Hardening audit (`security-hardening`) covers validation patterns and OWASP-aligned input handling.
- **Detail on fail:** `"3 dangling schemas: 'CreateUserSchema' in src/lib/schemas.ts (used only in z.infer), 'UpdateProfileSchema' in src/lib/schemas.ts (zero call sites). The schemas exist but no input is actually validated."`
- **Remediation:** A schema defined but never invoked is not validation — it's just types. The runtime accepts any input. Wire each schema into an actual parsing call at every entry point:

  ```ts
  // Bad: schema exists but is never parsed
  // src/lib/schemas.ts
  export const CreateUserSchema = z.object({ email: z.string().email() })

  // src/app/api/users/route.ts
  export async function POST(req: Request) {
    const body = await req.json()
    const user = await prisma.user.create({ data: body })  // unvalidated!
  }

  // Good: parse at the entry point
  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 prisma.user.create({ data })
  }
  ```

## External references

- cwe CWE-20
- owasp A03
- owasp A04

Taxons: placeholder-hygiene, injection-and-input-trust

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