Stack Guide Next.js Prisma

Next.js + Prisma Security

5 hidden security risks in the popular type-safe stack

Quick Answer

Prisma is type-safe by default, but has hidden risks: $queryRawUnsafe enables SQL injection, operator injection bypasses auth in findMany queries, and passing req.body directly to queries leaks data. AI tools often generate these patterns. Use tagged templates and type-cast all inputs.

Type-Safe
By Default
5
Hidden Risks
#1 ORM
TypeScript
Copy-Paste
Fixes Included

Why does Prisma security matter?

Prisma is the most popular ORM for Next.js and TypeScript projects. AI tools like Cursor, Bolt, and Claude Code love generating Prisma code because it's type-safe and produces clean, readable queries.

But type safety creates a false sense of security. TypeScript types are compile-time checks - they don't protect you at runtime. According to CWE-564, ORMs shift SQL injection risk but don't eliminate it. The attack surface moves from raw SQL to ORM-specific vulnerabilities.

For vibe coders, this is especially dangerous because vibe coded apps look safe (type errors would show in the IDE), but the runtime vulnerabilities remain invisible until exploited.

What makes Prisma different from raw SQL?

Think of Prisma as a translator between your TypeScript code and SQL. Most of the time, this translator is safe - it automatically uses parameterized queries. But you can force it to say dangerous things.

Prisma has two main attack surfaces that vibe coded apps need to watch for:

  • Raw query dangers: $queryRawUnsafe and Prisma.raw() bypass Prisma's safety mechanisms
  • Operator injection: Passing objects instead of primitives to queries allows attackers to inject query operators

According to the OWASP SQL Injection Prevention Cheat Sheet, parameterized queries are the primary defense. Prisma does this automatically - unless you opt out with unsafe functions.

What are the 5 hidden security risks?

These patterns are commonly generated by AI tools in vibe coded Next.js + Prisma apps:

The $queryRaw Confusion

Critical

Prisma has confusingly named functions: $queryRaw with tagged templates is SAFE, but $queryRawUnsafe and Prisma.raw() with user input are DANGEROUS. AI tools often generate the unsafe patterns. Reference: CWE-89

VULNERABLE
// VULNERABLE: $queryRawUnsafe with string interpolation
const email = req.body.email

const users = await prisma.$queryRawUnsafe(
  `SELECT * FROM "User" WHERE email = '${email}'`
)
// Attacker input: ' OR '1'='1

// ALSO VULNERABLE: Prisma.raw() defeats protection
const users = await prisma.$queryRaw`
  SELECT * FROM "User" WHERE email = ${Prisma.raw(email)}
`
SECURE
// SAFE: Tagged template literal (auto-parameterized)
const email = req.body.email

const users = await prisma.$queryRaw`
  SELECT * FROM "User" WHERE email = ${email}
`
// Prisma automatically creates prepared statement

// If you MUST use $queryRawUnsafe, use placeholders:
const users = await prisma.$queryRawUnsafe(
  'SELECT * FROM "User" WHERE email = $1',
  email  // Passed as parameter, not interpolated
)
Learn more about this vulnerability

Operator Injection Auth Bypass

Critical

Passing req.body directly to Prisma queries allows attackers to inject query operators. Sending password: {"not": ""} bypasses password checks because "not empty string" matches everything. Reference: CWE-20

VULNERABLE
// VULNERABLE: Direct object passthrough
const { email, password } = req.body

// Attacker sends: { password: { "not": "" } }
const user = await prisma.user.findFirst({
  where: { email, password }
})
// password: { not: "" } matches ANY non-empty password!

if (user) {
  // Attacker bypasses authentication!
  return signIn(user)
}
SECURE
// SECURE: Type-cast all inputs to primitives
const { email, password } = req.body

const user = await prisma.user.findFirst({
  where: {
    email: String(email),
    password: String(password)  // Rejects objects
  }
})

// BETTER: Use Zod for validation
import { z } from 'zod'

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
})

const validated = loginSchema.parse(req.body)
const user = await prisma.user.findFirst({
  where: validated
})
Learn more about this vulnerability

ORM Leak via Filter Passthrough

High

Passing req.query or req.body filters directly to Prisma allows attackers to access related model fields. They can leak sensitive data like password reset tokens through startsWith attacks. Reference: CWE-639

VULNERABLE
// VULNERABLE: Direct filter passthrough
// Attacker: GET /api/posts?filter[author][resetToken][startsWith]=abc

export async function GET(req: Request) {
  const { filter } = req.query

  const posts = await prisma.post.findMany({
    where: filter  // Attacker controls ENTIRE filter!
  })
  // Can probe resetToken values through response timing
}
SECURE
// SECURE: Whitelist allowed filter fields
export async function GET(req: Request) {
  const { searchParams } = new URL(req.url)
  const title = searchParams.get('title')
  const published = searchParams.get('published')

  const posts = await prisma.post.findMany({
    where: {
      ...(title && { title: { contains: String(title) } }),
      ...(published && { published: published === 'true' })
    }
  })
  // Only explicitly allowed fields can be filtered
}
Learn more about this vulnerability

Database URL Exposure

Critical

AI tools sometimes hardcode DATABASE_URL or commit .env files to git. Your database connection string contains credentials that grant full database access. Reference: CWE-798

VULNERABLE
// VULNERABLE: Hardcoded in source
// lib/prisma.ts
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: 'postgresql://user:[email protected]:5432/mydb'
    }
  }
})

// VULNERABLE: .env committed to git
// .env (in repo)
DATABASE_URL="postgresql://user:[email protected]:5432/mydb"
SECURE
// SECURE: Use environment variables
// .env.local (gitignored - NEVER commit)
DATABASE_URL="postgresql://user:[email protected]:5432/mydb"

// .gitignore
.env.local
.env*.local
.env

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

// Prisma reads DATABASE_URL from environment automatically
const prisma = new PrismaClient()

export default prisma
Learn more about this vulnerability

Missing Authorization in Server Actions

High

Prisma handles query building, not access control. AI-generated Server Actions often perform database operations without checking if the user owns the resource they are modifying. Reference: CWE-862

VULNERABLE
// VULNERABLE: No ownership check
'use server'

export async function updatePost(postId: string, content: string) {
  // Anyone can update any post!
  await prisma.post.update({
    where: { id: postId },
    data: { content }
  })
}
SECURE
// SECURE: Always verify ownership
'use server'

import { auth } from '@/lib/auth'

export async function updatePost(postId: string, content: string) {
  const session = await auth()
  if (!session?.user?.id) {
    throw new Error('Not authenticated')
  }

  // Verify user owns this post
  const post = await prisma.post.findUnique({
    where: { id: postId },
    select: { authorId: true }
  })

  if (post?.authorId !== session.user.id) {
    throw new Error('Not authorized')
  }

  await prisma.post.update({
    where: { id: postId },
    data: { content }
  })
}
Learn more about this vulnerability

AI fix prompt for Next.js + Prisma security

Copy this prompt and paste it into Cursor, Claude Code, or Bolt to automatically find and fix security vulnerabilities in your vibe coded project:

AI Security Fix Prompt
Review my Next.js + Prisma codebase for these security issues and fix them:

## 1. Raw Query Audit
Search for dangerous patterns:
- `$queryRawUnsafe` - should use parameterized placeholders ($1, $2), never string interpolation
- `$executeRawUnsafe` - same as above
- `Prisma.raw(` with variables - should only contain hardcoded values, never user input

## 2. Operator Injection Check
Find patterns where req.body or req.query is passed directly to Prisma:
- `prisma.*.findFirst({ where: req.body })`
- `prisma.*.findMany({ where: filter })` where filter comes from request
- `prisma.*.updateMany({ where: { ...req.body } })`

Fix by type-casting: `String(input)`, `Number(input)`, or use Zod validation.

## 3. Database URL Security
- Search for `DATABASE_URL` or `postgresql://` in any .ts, .js, or .env file that's committed
- Verify .env, .env.local, .env*.local are in .gitignore
- Check no datasources.db.url is hardcoded in prisma.ts

## 4. Server Action Authorization
For each file with `'use server'`:
- Verify authentication check: `auth()`, `getServerSession()`, or similar
- Verify ownership check before update/delete operations
- Check that postId, userId, etc. are validated against current user

## 5. Input Validation
Add Zod schemas for all Prisma query inputs:
```typescript
import { z } from 'zod'
const schema = z.object({ id: z.string().uuid() })
const validated = schema.parse(input)
```

After fixing, run these verification commands:
```bash
grep -r "queryRawUnsafe" --include="*.ts" --include="*.js"
grep -r "Prisma.raw" --include="*.ts" --include="*.js"
grep -r "DATABASE_URL" --include="*.ts" --include="*.js"
```

List all files you modified with before/after snippets.

For more context on raw query security, see the Prisma Raw Queries Documentation.

5-minute Prisma security audit

Use this checklist before deploying your vibe coded Next.js + Prisma app:

Raw Queries

Inputs

Secrets

Auth

For automated detection, run Vibeship Scanner to catch these issues in your codebase.

Scan your Next.js + Prisma app

Find raw query vulnerabilities, operator injection, and exposed secrets automatically

Scan your code free

Frequently asked questions

Is Prisma safe from SQL injection?

Yes, when used correctly. Prisma's standard query methods (findMany, create, update) use parameterized queries. The danger is in $queryRawUnsafe, $executeRawUnsafe, and using Prisma.raw() with user input. According to OWASP, ORMs shift SQL injection risk but don't eliminate it entirely.

What is the difference between $queryRaw and $queryRawUnsafe?

$queryRaw with tagged templates automatically creates prepared statements - it's safe. $queryRawUnsafe takes a plain string and does NO escaping - you must use parameterized placeholders ($1, $2). The "Unsafe" suffix is a deliberate warning from Prisma.

How do I secure my DATABASE_URL in Next.js?

Never commit DATABASE_URL to git. Use .env.local for local development (gitignored by default in Next.js). For production, set environment variables in your hosting platform (Vercel, Railway, etc.). Prisma reads DATABASE_URL automatically - no need to pass it explicitly.

Can Prisma be hacked?

Prisma itself is secure, but misuse creates vulnerabilities. The main risks are: $queryRawUnsafe with string interpolation, operator injection via object inputs, passing req.body directly to queries, and exposed DATABASE_URL. All are preventable with proper coding patterns.

Should I use raw SQL queries with Prisma?

Only when necessary - for complex queries, database-specific features, or performance optimization. When you do, use $queryRaw with tagged templates (safe) not $queryRawUnsafe with string interpolation (dangerous). Never use Prisma.raw() with user-controlled values.

Related content