Quick Answer
Prisma's query builder is safe from SQL injection by default. The problem is that AI tools reach for raw SQL, and build it with string interpolation in $queryRawUnsafe and $executeRawUnsafe, whenever the standard API does not cover a use case, which reintroduces the exact injection vulnerabilities that Prisma was designed to prevent. AI-generated code also often exposes Prisma error messages containing schema details to API consumers.
Prisma Is Safe Until AI Makes It Unsafe
Prisma is one of the most widely used TypeScript ORMs. Its type-safe query builder prevents SQL injection by design - every query is parameterized automatically. This is excellent for security.
The problem: whenever AI needs a query that Prisma's query builder does not directly support (full-text search, complex aggregations, raw SQL functions), it falls back to raw SQL built with string interpolation. Injection is still in the OWASP Top 10 (A05 in 2025). Prisma's $queryRawUnsafe and $executeRawUnsafe pass that string to the database as it is, bypassing Prisma's built-in protections.
CodeRabbit's State of AI vs Human Code Generation Report (December 2025) found security issues up to 2.74x higher in AI co-authored pull requests than in human-only ones. In a Prisma codebase, raw queries are where SQL injection can come back in.
The Unsafe $queryRaw Pattern
// ❌ BAD - AI interpolates user input into $queryRawUnsafe
const searchProducts = async (term) => {
return prisma.$queryRawUnsafe(
`SELECT * FROM products WHERE name LIKE '%${term}%'`
);
};
// ❌ BAD - AI builds dynamic queries with concatenation
const getFilteredUsers = async (role, status) => {
let query = 'SELECT * FROM users WHERE 1=1';
if (role) query += ` AND role = '${role}'`;
if (status) query += ` AND status = '${status}'`;
return prisma.$queryRawUnsafe(query);
};
// ✅ GOOD - Use tagged template (Prisma.sql) for safe parameterization
const searchProducts = async (term) => {
return prisma.$queryRaw(
Prisma.sql`SELECT * FROM products WHERE name LIKE ${`%${term}%`}`
);
};
// ✅ GOOD - Use Prisma's query builder for dynamic filters
const getFilteredUsers = async (role, status) => {
return prisma.user.findMany({
where: {
...(role && { role }),
...(status && { status }),
},
});
};
The Critical Difference: Tagged Template vs String
Prisma has two raw query methods that look almost identical but have completely different security properties. In Prisma ORM 7, $queryRaw and $executeRaw only accept a tagged template or Prisma.sql, and every ${value} in them becomes a query parameter (Prisma's raw query docs):
| Method | Usage | Safe? | Why |
|---|---|---|---|
$queryRaw with tagged template |
$queryRaw(Prisma.sql`...${var}...`) |
Yes | Tagged template auto-parameterizes |
$queryRaw with string |
$queryRaw(`...${var}...`) |
Rejected | Prisma throws "$queryRaw is a tag function"; switching to $queryRawUnsafe to silence the error is how injection gets in |
$queryRawUnsafe |
$queryRawUnsafe(str) |
NO | Runs the string as written; safe only if values go in as separate parameters ($1, ?) |
$executeRaw with tagged template |
$executeRaw(Prisma.sql`...`) |
Yes | Same as $queryRaw tagged template |
$executeRawUnsafe |
$executeRawUnsafe(str) |
NO | Same problem as $queryRawUnsafe |
Schema Exposure Through Error Messages
AI-generated API routes often pass Prisma errors directly to the client. Prisma's error messages include table names, column names, constraint details, and sometimes partial query text. This information helps attackers map your database schema.
// ❌ BAD - Exposing Prisma errors to the client
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({ data: req.body });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
// Reveals: "Unique constraint failed on the fields: (`email`)"
// Foreign key errors name the violated constraint or field too
}
});
// ✅ GOOD - Map Prisma errors to safe responses
import { Prisma } from '@prisma/client';
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({ data: req.body });
res.json(user);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return res.status(409).json({ error: 'This email is already registered' });
}
}
console.error('User creation failed:', error);
res.status(500).json({ error: 'Failed to create user' });
}
});
Safe Alternatives to Raw Queries
Most raw query usage in AI-generated code is unnecessary. Prisma's query builder handles the vast majority of use cases:
- Full-text search: Use Prisma's
searchfilter (on PostgreSQL it needs thefullTextSearchPostgrespreview feature) orcontainsfor simple cases - Complex aggregations: Use Prisma's
aggregate,groupBy, andcountmethods - Dynamic filtering: Build
whereobjects conditionally in TypeScript - Upserts: Use
prisma.model.upsert()instead of raw INSERT ON CONFLICT
How to Audit Your Prisma Code
- Search for
$queryRawand$executeRawin your codebase. Check that each one uses a tagged template orPrisma.sql, and that no user input is passed throughPrisma.raw(), which Prisma's docs warn can reintroduce injection. - Search for
$queryRawUnsafeand$executeRawUnsafe. These are dangerous whenever user input is built into the string. Replace them with tagged template alternatives or Prisma query builder methods, or at least pass values as separate parameters ($1,?). - Check error handlers for
error.messagebeing sent to the client. Map Prisma error codes to user-friendly messages. - Run automated scanning. VibeDoctor's Vibe Check flags SQL built with string interpolation in
.query(),.execute(),.raw()and Knex-stylewhereRaw()calls (SEC-002). It does not check Prisma's$queryRawUnsafeand$executeRawUnsafecalls yet, so steps 1 and 2 are still the way to find those. Free to sign up.
FAQ
Is Prisma safe from SQL injection by default?
Yes, when using the query builder (prisma.model.findMany(), create(), etc.). All queries are parameterized automatically. The risk comes from $queryRawUnsafe / $executeRawUnsafe with interpolated input, and from user input passed through Prisma.raw(). If you never use raw queries, you are safe from SQL injection.
When should I actually use $queryRaw?
Rarely. Common legitimate uses: database-specific functions not supported by Prisma (PostGIS spatial queries, full-text search with ts_rank), complex CTEs, and performance-critical queries that Prisma generates suboptimally. Even then, always use the tagged template syntax (Prisma.sql) for parameterization.
Does Drizzle ORM have the same issues?
Drizzle has similar raw query escape hatches (sql.raw()) that AI tools can misuse. However, Drizzle's main API is a typed query builder, and its sql`query ${param}` template turns every value into a query parameter, so the safe pattern is the default. Both ORMs are safe when used correctly; the risk is in the raw query fallbacks that AI reaches for.
Can Prisma's query builder be used for all queries?
For most application queries, yes. Prisma supports filtering, sorting, pagination, relations, aggregations, grouping, transactions, and upserts through its type-safe API. The rest, highly database-specific operations, can use $queryRaw with tagged templates safely.