Prisma orm security
Skill hlsitechio/claude-skills-security/appsec-stack-pack/prisma-orm-security
Defensive security audit skills for Claude — tech-stack-keyed and audit-domain-keyed packs for SaaS apps.
npx -y skills add hlsitechio/claude-skills-security --skill prisma-orm-securityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Security audit specific to Prisma ORM usage including raw query escape hatches ($queryRaw, $executeRaw, $queryRawUnsafe), mass assignment via spreading user input into create/update, missing tenant scoping on findFirst/findMany, IDOR through Prisma query construction, schema-level access control gaps, and Prisma Accelerate/Pulse security considerations. Use this skill whenever the user mentions Prisma, prisma/client, schema.prisma, $queryRaw, the prisma.modelName.create / update / findMany pattern, or asks "audit my Prisma queries", "Prisma security review", "raw query safety", "Prisma mass assignment". Trigger when the codebase contains @prisma/client, schema.prisma, or any prisma. query calls.
SKILL.md
10.5 KB, as published. Nobody here has run it
Prisma ORM Security Audit
Audit Prisma ORM usage for vulnerabilities specific to its query model, raw query escape hatches, and common patterns developers get wrong.
When this skill applies
- Reviewing code using
@prisma/client - Auditing
$queryRaw/$executeRaw/$queryRawUnsafecalls - Reviewing mass assignment patterns in
create/update/upsert - Checking tenant scoping across queries
- Reviewing
schema.prismafor missing constraints or indexes that have security implications
Use other skills for: generic IDOR/BOLA patterns (saas-security-pack/saas-code-security-review/references/idor-bola-patterns.md), tenant isolation (saas-security-pack/saas-tenant-isolation), backend framework specifics (nestjs-security, nodejs-express-security, nextjs-security).
Workflow
Follow ../_shared/audit-workflow.md. Prisma-specific notes below.
Phase 1: Stack detection
grep -E '"@prisma/client":' package.json
find . -name 'schema.prisma' -not -path '*/node_modules/*'
prisma --version 2>/dev/null
Phase 2: Inventory
# Raw query usage
grep -rnE '\$queryRaw|\$executeRaw|\$queryRawUnsafe|\$executeRawUnsafe' src/
# Model accesses
grep -rnE 'prisma\.[a-zA-Z]+\.(create|update|upsert|delete|findFirst|findMany|findUnique)' src/ | head -50
# Spread patterns (mass assignment risk)
grep -rnE 'data:\s*{?\s*\.\.\.' src/ | head -30
# Transaction / interactiveTransaction
grep -rnE '\$transaction|interactiveTransaction' src/
# Soft-delete or middleware
grep -rnE 'prisma\.\$(use|extends)' src/
Phase 3: Detection — the checks
Raw query injection
Prisma's parameterized queries are safe; the escape hatches are not.
- PRI-SQL-1
$queryRaw\...`(tagged template) — safe when used as designed:prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`. The tagged template parameterizes${...}`. - PRI-SQL-2
$queryRawUnsafe(...)and$executeRawUnsafe(...)— NOT safe with user input. These accept a string + separate args; the string is concatenated. Audit every call:// BAD await prisma.$queryRawUnsafe(`SELECT * FROM ${tableName} WHERE id = ${userId}`); // GOOD await prisma.$queryRaw`SELECT * FROM "Posts" WHERE id = ${userId}`; // or, if table name truly dynamic: const allowed = ['posts', 'comments', 'tags']; if (!allowed.includes(tableName)) throw new Error('invalid table'); await prisma.$queryRawUnsafe(`SELECT * FROM "${tableName}" WHERE id = $1`, userId); - PRI-SQL-3 Dynamic ORDER BY / LIMIT / column names in raw queries — Prisma's parameterization doesn't cover those; allowlist explicitly.
- PRI-SQL-4
Prisma.sqltemplate literal helper — safe for composition:Prisma.sql`WHERE id = ${id}composed into larger queries.
// Composing safely
import { Prisma } from '@prisma/client';
const filters = [];
if (status) filters.push(Prisma.sql`status = ${status}`);
if (userId) filters.push(Prisma.sql`user_id = ${userId}`);
const whereClause = filters.length
? Prisma.sql`WHERE ${Prisma.join(filters, ' AND ')}`
: Prisma.empty;
await prisma.$queryRaw`SELECT * FROM "Posts" ${whereClause}`;
Mass assignment
Spreading user input into Prisma's data object is mass assignment.
- PRI-MA-1
data: { ...req.body }patterns — User can set any column they want, includingrole: 'admin',tenantId: 'other-tenant',verified: true,createdAt: '1970-01-01'. - PRI-MA-2 Even with strict-typed inputs, if the input type matches the model type, user fields override server expectations.
// BAD
await prisma.user.create({ data: { ...req.body } });
// BAD even with parsing
const parsed = UserSchema.parse(req.body); // schema includes role, verified, ...
await prisma.user.create({ data: parsed });
// GOOD — explicit allow-list at the boundary
const { displayName, email, bio } = UserSchema.pick({
displayName: true, email: true, bio: true,
}).parse(req.body);
await prisma.user.create({
data: {
displayName,
email,
bio,
role: 'user', // server-controlled
tenantId: session.tenantId, // from session
},
});
- PRI-MA-3 Updates more dangerous than creates — an attacker can flip flags on their own record (e.g.,
data: { isVerified: true }). - PRI-MA-4 Nested writes via
createandconnect—data: { posts: { create: { ... } } }— also subject to mass assignment if user controls the nested object.
IDOR — missing scoping in queries
Prisma doesn't auto-scope. Every findUnique / findFirst / findMany / update / delete must include ownership check.
-
PRI-IDOR-1
findUnique({ where: { id: req.params.id } })— finds any record by id. If returned to user, IDOR. -
PRI-IDOR-2
update({ where: { id: req.params.id }, data: ... })— same; updates any record. -
PRI-IDOR-3 Correct pattern: compound where:
// BAD await prisma.invoice.findUnique({ where: { id: invoiceId } }); // GOOD await prisma.invoice.findFirst({ where: { id: invoiceId, tenantId: session.tenantId }, });Note:
findUniquerequires the where to be a unique identifier; if you need a compound where with non-unique fields, usefindFirst. For Prisma 5+: usefindUniqueOrThrow/findFirstOrThrowfor safer error handling. -
PRI-IDOR-4
findManywithout where → returns all rows. Always pass a tenant filter at minimum. -
PRI-IDOR-5 Bulk operations (
updateMany,deleteMany) — same scoping rule.prisma.post.updateMany({ where: { authorId: session.userId }, data: ... }).
Soft delete and visibility
- PRI-SD-1 Soft-delete columns (e.g.,
deletedAt) — queries don't automatically filter; adddeletedAt: nullto every where. Or use Prisma Client Extensions / middleware to apply globally. - PRI-SD-2 Tombstone records returned to clients leak existence of deleted resources.
Sensitive fields in responses
- PRI-RES-1
passwordHash,mfaSecret,apiKeyHash, billing details, internal flags — never returned to clients. Useselectto explicitly choose returned fields, or define DTO projections.// BAD — returns whole user including passwordHash const user = await prisma.user.findUnique({ where: { id } }); return user; // GOOD const user = await prisma.user.findUnique({ where: { id }, select: { id: true, displayName: true, email: true, createdAt: true }, }); - PRI-RES-2 Prisma Client Extensions can define safe-by-default projections; check for an extension that filters sensitive fields on
findMany/findFirst.
Database connection and secrets
- PRI-DB-1
DATABASE_URLnot in client-side code orVITE_/NEXT_PUBLIC_env vars. - PRI-DB-2 Connection pool limits set (
?connection_limit=Nin URL or viadatasourceconfig) — unbounded connections become DoS surface. - PRI-DB-3 SSL required in production (
?sslmode=requireor?sslmode=verify-full). - PRI-DB-4 Read replicas use the read-only role; not the migration-capable role.
- PRI-DB-5 Prisma Migrate's shadow database not on the production cluster (separate db needed for migrate dev / migrate diff).
Schema-level concerns
Open schema.prisma:
- PRI-SCH-1 Foreign keys defined (
onDelete: Cascade/Restrict/SetNull) — orphaned records become security issues (e.g., a post for a deleted user with a staleauthorId). - PRI-SCH-2 Required relations marked correctly — optional relations (
?) often hide cases where the application assumed presence. - PRI-SCH-3 Indexes on columns used in WHERE — performance, but also DoS prevention (unindexed query on large table → table scan → resource exhaustion).
- PRI-SCH-4 Unique constraints on identifier columns (
@uniqueonemail,slug, etc.) — without them, race conditions allow duplicates with security implications. - PRI-SCH-5 Multi-tenant schemas: tenant_id columns on every shared table; foreign-key composite indexes including
tenant_id. - PRI-SCH-6
@db.Text/@db.VarChar(N)— large unbounded text columns enable DoS via huge writes.
Prisma extensions and middleware
Use of Client Extensions / deprecated $use middleware can implement cross-cutting controls:
- PRI-EXT-1 Tenant-scoping extension that auto-injects
tenantIdfilter — good defense-in-depth. - PRI-EXT-2 Logging extension that doesn't log sensitive fields (Prisma
loglevels include query and params — confirm not enabled in production for queries containing PII). - PRI-EXT-3 Soft-delete extension applied to all relevant models.
Prisma Accelerate / Data Proxy
- PRI-ACC-1 Prisma Accelerate uses a connection string with API key. Treat as a secret; rotate periodically; restrict by project.
- PRI-ACC-2 Accelerate caching — confirm cache strategy doesn't share between tenants. The
cacheStrategyis opt-in per query; ensure tenant context is in cache key. - PRI-ACC-3 Edge runtime use (Vercel Edge, Cloudflare Workers) requires Accelerate or Data Proxy; verify the Edge bundle doesn't ship the direct DATABASE_URL.
Migrations
- PRI-MIG-1 Migrations applied via CI/CD with limited credentials (not the runtime app credential).
- PRI-MIG-2 Destructive migrations gated by approval; reviewed for accidental column drops on populated tables.
- PRI-MIG-3
prisma db pushfor production = bad practice (skips migration history).
Phase 4: Triage
Critical class examples:
$queryRawUnsafewith user-concatenated inputdata: { ...req.body }on user-self updates including admin flags- Every
findFirst/update/deletemissing tenant scope - Returning whole user records including password hashes
Phase 5: Report
Use ../_shared/findings-schema.md. Prefix IDs with PRI-.
References
references/raw-query-safety.md— Detailed$queryRawvs$queryRawUnsafepatterns and Prisma.sql composition