Mutating routes validate input before processing
Why it matters
A POST handler that pipes req.body directly into prisma.user.create({ data: body }) accepts whatever JSON the client sends — extra fields, injected relations, overridden IDs. CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) and OWASP A03 describe this exactly: an attacker can escalate privileges by setting role: 'admin' or inject unexpected columns. TypeScript types don't run at runtime; only an explicit .parse() call rejects malformed or malicious input before the write commits.
Severity rationale
High because unvalidated POST/PUT/PATCH/DELETE bodies enable mass-assignment attacks and arbitrary data injection into the database on every mutating endpoint.
Remediation
Validate the request body before any database write. The validation must appear above the first prisma.* call in the handler's execution path.
import { z } from 'zod'
const Body = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
})
export async function POST(req: Request) {
const body = await req.json()
const data = Body.parse(body) // unknown fields stripped; invalid shapes rejected
const user = await prisma.user.create({ data })
return Response.json(user, { status: 201 })
}
If you're using tRPC, the .input() call handles this. For Server Actions with next-safe-action, the schema option covers it. The key invariant: no path through the handler should reach a DB write without first passing through a schema parse.
Detection
-
ID:
mutating-routes-validate-input -
Severity:
high -
What to look for: Walk all mutating handlers (API handlers exporting
POST,PUT,PATCH, orDELETE). Count all mutating handlers found. For each, count whether the handler body contains at least 1 of these validation invocation patterns BEFORE the first database write call (prisma.X.create,prisma.X.update,prisma.X.upsert,prisma.X.delete,db.insert(,db.update(,db.delete(,Model.create(, raw SQLINSERT/UPDATE/DELETE,fetchto a third-party withreq.bodypayload): a.parse(call, a.safeParse(call, a Zod.parseAsync(, a Joi.validate(, a Yup.validate(, a Valibotparse(, a Superstructassert(, a class-validatorvalidate(, OR a middleware-applied resolver/validator from a known framework helper (zValidator,validator(...),safeAction,parseWithZod). -
Pass criteria: 100% of mutating handlers either invoke validation before the first database write OR have no database write at all. Report: "X mutating handlers inspected, Y validate before DB write, 0 unvalidated."
-
Fail criteria: At least 1 mutating handler performs a database write without first validating input.
-
Do NOT pass when: A handler reads
req.bodyand passes it directly toprisma.X.create({ data: body })without parsing through a schema. Even if Prisma's TypeScript types catch some shape errors, the runtime accepts whatever JSON the client sends. -
Skip (N/A) when: Project has 0 API handler files OR no mutating handlers exist.
-
Detail on fail:
"2 mutating handlers without input validation: app/api/users/route.ts POST passes req.body directly to prisma.user.create, app/api/posts/route.ts PUT updates with unvalidated body" -
Remediation: Every mutating endpoint is an attack surface. Validate input before any side effect:
import { z } from 'zod' const Body = z.object({ email: z.string().email(), name: z.string().min(1) }) export async function POST(req: Request) { const body = await req.json() const data = Body.parse(body) // throws 400 if invalid const user = await prisma.user.create({ data }) return Response.json(user, { status: 201 }) }
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-18·v1.0.0·Initial import from ai-slop-security-theater·automated