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
| Aspect | Details |
|---|---|
| CWE Reference | CWE-284 (Improper Access Control) |
| Severity | High (CVSS 7.5) |
| Auto-Fix | ❌ No auto-fix available |
| Category | Security |
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{ "jsPlugins": ["oxlint-plugin-prisma-security"], "rules": { "prisma-security/no-unscoped-mutation": "error" }}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.