← All Articles 📝 Code Patterns Critical By Nishant Kumar Verma Updated

Prisma in AI-Generated Code: Raw Query Injection and Schema Exposure Risks

Prisma is the ORM of choice for Bolt, Lovable, and Cursor. But AI-generated Prisma code uses $queryRaw unsafely and exposes schema details.

SEC-002 SEC-010

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 search filter (on PostgreSQL it needs the fullTextSearchPostgres preview feature) or contains for simple cases
  • Complex aggregations: Use Prisma's aggregate, groupBy, and count methods
  • Dynamic filtering: Build where objects conditionally in TypeScript
  • Upserts: Use prisma.model.upsert() instead of raw INSERT ON CONFLICT

How to Audit Your Prisma Code

  1. Search for $queryRaw and $executeRaw in your codebase. Check that each one uses a tagged template or Prisma.sql, and that no user input is passed through Prisma.raw(), which Prisma's docs warn can reintroduce injection.
  2. Search for $queryRawUnsafe and $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, ?).
  3. Check error handlers for error.message being sent to the client. Map Prisma error codes to user-friendly messages.
  4. Run automated scanning. VibeDoctor's Vibe Check flags SQL built with string interpolation in .query(), .execute(), .raw() and Knex-style whereRaw() calls (SEC-002). It does not check Prisma's $queryRawUnsafe and $executeRawUnsafe calls 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.

Diagnose your codebase - free

VibeDoctor checks for SEC-002, SEC-010 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →