Skip to main content
interlace
Plugin: drizzle-securityRules

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

AspectDetails
CWE ReferenceCWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes)
SeverityHigh (CVSS 8.1)
Auto-Fix❌ No auto-fix available
CategorySecurity

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 body or query key. form.body and config.query are ordinary application objects. The chain has to bottom out in a request-shaped identifier (req, request, ctx, context, event).
  • ctx.data / context.data. data is 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) or repo.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.params or p.payload (and Koa's p.request.body) on a function parameter, await p.json() / await p.formData() on the Fetch Request, 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 ...rest of a destructured payload. The request copied onto an entity with Object.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, a Headers, 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

Use this rule

npm install -D oxlint-plugin-drizzle-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-drizzle-security"],  "rules": {    "drizzle-security/no-mass-assignment": "error"  }}
vite.config.ts
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.