Skip to main content
interlace
Plugin: prisma-securityRules

no-unscoped-mutation

Require a `where` filter on Prisma bulk mutations, so deleteMany and updateMany cannot rewrite or delete every row in the table.

CWE: CWE-284 OWASP: A01:2021 – Broken Access Control

Detects Prisma bulk mutations that reach every row in the table. This rule is part of eslint-plugin-prisma-security.

💼 This rule is set to error in the strict config.

Quick Summary

AspectDetails
CWE ReferenceCWE-284 (Improper Access Control)
SeverityHigh (CVSS 7.5)
Auto-Fix❌ No auto-fix available
CategorySecurity

Why this matters

A bulk mutation without a filter is one forgotten clause away from rewriting or deleting the entire table. It type-checks, it passes review, and it usually only shows up once it has run against production data. Prisma has no instance-mutation methods, so deleteMany() is always the bulk form and a call with no filter is always unscoped.

❌ Incorrect

// Deletes every user in the table
await prisma.user.deleteMany();

// Empty options is not a filter
await prisma.user.deleteMany({});

// Grants admin to every row
await prisma.user.updateMany({ data: { role: 'admin' } });

✅ Correct

await prisma.user.deleteMany({ where: { active: false } });

await prisma.user.updateMany({
  where: { authorId },
  data: { role: 'admin' },
});

// Single-record operations are inherently scoped
await prisma.user.delete({ where: { id } });

Known limitations

This rule reports only what it can prove. Scope that cannot be read statically is treated as present, so the rule stays silent rather than guessing.

A filter built elsewhere (prisma.user.deleteMany(buildFilter(req.query))) cannot be read statically and is deliberately not reported — see Known limitations.

When not to use it

Disable this rule in maintenance scripts, seeders, and test fixtures whose job is to clear a table. Prefer a scoped eslint-disable-next-line on the specific call over switching the rule off for the whole project.

How a mutation is recognised (2026-10 review)

The receiver is judged by where it is written — an import of the driver, a client built from it, a transaction handle it passes to a callback, a typed parameter or class member, or a relative module that exports one — never by its name. A Map or Set is never a table; an untraceable client reports when the call reads a model off it (db.user.deleteMany()).

Test files are skipped.

Further reading

Use this rule

npm install -D oxlint-plugin-prisma-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-prisma-security"],  "rules": {    "prisma-security/no-unscoped-mutation": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-prisma-security'],    rules: {      'prisma-security/no-unscoped-mutation': 'error',    },  },});

oxlint-plugin-prisma-security re-exports eslint-plugin-prisma-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.