no-mass-assignment
Disallow writing an inbound request object straight to the database through Drizzle, which lets the caller set every column the model exposes.
CWE: CWE-915 OWASP: A04:2021 – Insecure Design
Detects an inbound request object — or a spread of one — reaching a Drizzle write. This rule is part of eslint-plugin-drizzle-security.
💼 This rule is set to error in the strict config.
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) |
| Severity | High (CVSS 8.1) |
| Auto-Fix | ❌ No auto-fix available |
| Category | Security |
Why this matters
await db.insert(users).values(req.body);That line updates the fields the endpoint is about. It also updates every
other column on the model: role, isAdmin, ownerId, emailVerified,
credits, stripeCustomerId. None of them appear in the diff, which is why
this passes review — the vulnerability is in what the code does not say.
It is also one of the few defects that gets worse without anyone touching it.
Add a role column to the model six months from now and every existing
mass-assignment site silently starts accepting it. No line changes; the
exposure is new. That is what makes this worth a lint rule rather than a code
review habit.
Drizzle takes the row directly on .values() and .set(), so the argument itself is the payload.
❌ Incorrect
// ❌ the whole request object
await db.insert(users).values(req.body);
// ❌ spreading it is the same thing
await db
.update(users)
.set({ ...req.body })
.where(eq(users.id, id));✅ Correct
// ✅ name the columns this endpoint owns
await db.insert(users).values({ name: req.body.name, email: req.body.email });
// ✅ or validate into a typed object first
const input = insertUserSchema.parse(req.body);
await db.insert(users).values(input);What this rule deliberately does not report
- A payload that names its fields.
{ name: req.body.name }reads one value out of the request; it is the fix, and it is silent. Note that a named field beside a spread does not help —{ ...req.body, updatedAt }still carries everything the spread brought. - An object that merely has a
bodyorquerykey.form.bodyandconfig.queryare ordinary application objects. The chain has to bottom out in a request-shaped identifier (req,request,ctx,context,event). ctx.data/context.data.datais ordinary application state in several frameworks, so it is not treated as a request surface — a deliberate false negative in exchange for not reporting code with no request in it.- A value it cannot see through.
repo.create(validated)orrepo.create(buildInput(req))may still be unsafe, but the rule cannot prove it and will not guess. Guessing is how a security rule earns a false-positive reputation. - A file that never imports drizzle-orm. The driver import is the gate that keeps this rule inside its own plugin.
When Not To Use It
There is no configuration in which handing the raw request to a write is correct, so this rule has no options — and deliberately so. An allowlist option would let a project re-approve the dangerous shape wholesale, one config file further from the call site, which is the same mistake with more steps.
If a specific call is genuinely safe — an internal job with a payload you construct yourself — disable it there with a reason:
// eslint-disable-next-line drizzle-security/no-mass-assignment -- payload is built in-process, not from a request
await db.insert(users).values(req.body);How the request and the write are recognised (2026-10 review)
Neither is decided by a variable's name.
- The request is where a handler receives it:
p.body,p.query,p.paramsorp.payload(and Koa'sp.request.body) on a function parameter,await p.json()/await p.formData()on the FetchRequest,JSON.parse(event.body), a parameter decorated with@Body()/@Query()/@Param()(NestJS, routing-controllers), a handler that destructures({ body }), a local alias of any of these, and the...restof a destructured payload. The request copied onto an entity withObject.assign(entity, req.body)before the write is reported too. - The write is judged by where its receiver is written — an import of the
driver, a client built from it, a transaction handle it passes to a
callback, a parameter or class member typed with its types, a model class
that extends its base, or a relative module that exports one. A
Map, aHeaders, a crypto hash or another library's client is never a write; an untraceable handle reports when the write follows the statement verb —db.insert(t).values(…),db.update(t).set(…).
Test files are skipped.
Further Reading
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
- OWASP A04:2021 – Insecure Design
- OWASP: Mass Assignment Cheat Sheet
Use this rule
npm install -D oxlint-plugin-drizzle-security{ "jsPlugins": ["oxlint-plugin-drizzle-security"], "rules": { "drizzle-security/no-mass-assignment": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-drizzle-security'], rules: { 'drizzle-security/no-mass-assignment': 'error', }, },});oxlint-plugin-drizzle-security re-exports eslint-plugin-drizzle-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.