How to Prevent SQL Injection in Next.js — A Complete Guide
SQL injection remains one of the most dangerous web vulnerabilities, even in modern Next.js applications. Whether you use Prisma, Drizzle, or raw SQL, here is how to keep your queries safe — and how to detect injection risks automatically.
Why Next.js Apps Are Still Vulnerable
Next.js developers often assume ORMs like Prisma eliminate SQL injection risk entirely. That is mostly true for standard Prisma queries. But the moment you use $queryRaw, $executeRaw, or drop down to a lower-level database driver, the protection disappears.
Server Actions in Next.js 14+ make this even more relevant. With server-side code running directly in your component files, developers sometimes write database queries that accept form data without sanitization. The boundary between client and server code is blurring, and SQL injection is sneaking through the gaps.
The Safe Way: Prisma Parameterized Queries
Standard Prisma Client queries are automatically parameterized. This code is safe:
// SAFE: Prisma parameterizes automatically
const user = await prisma.user.findFirst({
where: {
email: userInput, // automatically escaped
},
});
// SAFE: Prisma parameterizes findMany too
const posts = await prisma.post.findMany({
where: {
title: { contains: searchQuery }, // safe
},
});Prisma generates parameterized SQL under the hood. The user input never touches the query structure. ShipSafe recognizes these patterns and does not flag them — this is one of the Prisma-specific exceptions added in v1.0.4.
The Dangerous Way: Raw Queries
Raw queries bypass Prisma’s protections. This is where injection happens:
// VULNERABLE: string interpolation in raw query
const users = await prisma.$queryRaw`
SELECT * FROM users
WHERE name = '${searchQuery}'
`;
// VULNERABLE: string concatenation
const result = await prisma.$queryRawUnsafe(
"SELECT * FROM users WHERE email = '" + email + "'"
);The safe way to use raw queries is with Prisma’s tagged template:
// SAFE: Prisma tagged template auto-parameterizes
import { Prisma } from "@prisma/client";
const users = await prisma.$queryRaw`
SELECT * FROM users
WHERE name = ${searchQuery}
`;
// Prisma converts this to a parameterized query:
// SELECT * FROM users WHERE name = $1The difference is subtle but critical. When you wrap the interpolation in quotes ('${searchQuery}'), Prisma treats it as a literal string. Without quotes, Prisma parameterizes it properly.
Server Actions: A New Attack Surface
Next.js Server Actions run on the server but are invoked from the client. Any form input that reaches a database query needs validation:
// VULNERABLE: Server Action with unvalidated input
"use server";
export async function searchUsers(formData: FormData) {
const query = formData.get("search") as string;
// Direct interpolation into raw SQL
const users = await db.execute(
`SELECT * FROM users WHERE name LIKE '%${query}%'`
);
return users;
}// SAFE: Validated and parameterized
"use server";
import { z } from "zod";
const searchSchema = z.object({
search: z.string().max(100).regex(/^[a-zA-Z0-9\s]+$/),
});
export async function searchUsers(formData: FormData) {
const { search } = searchSchema.parse({
search: formData.get("search"),
});
const users = await prisma.user.findMany({
where: { name: { contains: search } },
});
return users;
}Drizzle ORM: Same Principles Apply
If you use Drizzle instead of Prisma, the same rules apply. Drizzle’s query builder is safe; raw SQL is not:
// SAFE: Drizzle query builder
const users = await db
.select()
.from(usersTable)
.where(eq(usersTable.email, userInput));
// VULNERABLE: Drizzle raw SQL with interpolation
const users = await db.execute(
sql.raw(`SELECT * FROM users WHERE email = '${userInput}'`)
);
// SAFE: Drizzle parameterized raw SQL
const users = await db.execute(
sql`SELECT * FROM users WHERE email = ${userInput}`
);Automated Detection with ShipSafe
ShipSafe has 127 SQL injection detection rules that catch all the patterns above. It understands the difference between safe Prisma queries and dangerous raw SQL:
$ shipsafe scan
CRITICAL sql-injection/prisma-raw-unsafe
src/actions/search.ts:8
$queryRawUnsafe with string concatenation containing user input.
Fix: Use tagged template — prisma.$queryRaw`SELECT ... WHERE id = ${id}`
HIGH sql-injection/template-literal-in-query
src/lib/db.ts:15
User input interpolated in SQL template literal with quotes.
Fix: Remove quotes around interpolation to enable parameterization.
Scan complete: 2 findings (1 critical, 1 high)ShipSafe’s Tree-sitter AST analysis traces the data flow from form input to database query, catching injection vectors that regex-based scanners miss.
Prevention Checklist
- Use ORM queries by default. Prisma’s
findMany,findFirst,createare always safe. - Parameterize raw queries. Use tagged templates without quotes around interpolated values.
- Validate all input. Use Zod or a similar library to validate form data before it touches any query.
- Avoid
$queryRawUnsafe. If you must use it, build the query string with only trusted values. - Scan automatically. Install ShipSafe git hooks to catch injection before it reaches your repository.
Catch SQL Injection Automatically
ShipSafe has 127 SQL injection rules with Prisma and Next.js awareness.
npm install -g @shipsafe/cli && shipsafe scanGet Started Free