Back to guides

SQL Injection Prevention with Prisma

By Davy Rogers

Prisma's query builder prevents most SQL injection by default. But raw queries, dynamic filters, and $queryRawUnsafe can quietly reintroduce the vulnerabilities you thought you'd left behind. Below: the safe patterns and the dangerous ones.

How Prisma Prevents SQL Injection

Prisma's standard query methods generate parameterized SQL:

// This is safe - Prisma parameterizes the input
const user = await prisma.user.findUnique({
  where: { email: userInput },
});

// Generated SQL: SELECT * FROM "User" WHERE "email" = $1
// The value is passed as a parameter, not concatenated

No matter what userInput contains - including '; DROP TABLE users; -- - it is treated as a value, not SQL.

Reference: Prisma - SQL Injection Prevention

The Danger: $queryRawUnsafe

Prisma provides $queryRawUnsafe for cases where you need fully dynamic SQL. It does no parameterization:

// DANGEROUS - direct string concatenation
const users = await prisma.$queryRawUnsafe(
  `SELECT * FROM "User" WHERE name = '${userInput}'`
);

If userInput is ' OR '1'='1, this returns all users. If it is '; DROP TABLE "User"; --, your table is gone.

Never use $queryRawUnsafe with user input. Use $queryRaw with tagged templates instead.

Safe Raw Queries with $queryRaw

$queryRaw uses tagged template literals. Values interpolated with ${} are automatically parameterized:

// SAFE - Prisma parameterizes the interpolated values
const users = await prisma.$queryRaw`
  SELECT * FROM "User"
  WHERE name = ${userInput}
  AND age > ${minAge}
`;

Critical: this only works with the tagged template syntax. Do not construct the string separately:

// BAD - this defeats the parameterization
const query = `SELECT * FROM "User" WHERE name = '${userInput}'`;
const users = await prisma.$queryRaw(Prisma.raw(query));

Reference: Prisma - Raw Queries

Dynamic Column Names and Table Names

You cannot parameterize column names, table names, or SQL keywords. These require allowlists:

// Column names cannot be parameterized
const ALLOWED_SORT_COLUMNS = ["name", "email", "createdAt"] as const;
type SortColumn = typeof ALLOWED_SORT_COLUMNS[number];

function buildOrderBy(column: string): SortColumn {
  if (!ALLOWED_SORT_COLUMNS.includes(column as SortColumn)) {
    throw new Error(`Invalid sort column: ${column}`);
  }
  return column as SortColumn;
}

// Use the validated column
const validColumn = buildOrderBy(userSortInput);
const users = await prisma.user.findMany({
  orderBy: { [validColumn]: "asc" },
});

For raw SQL:

const ALLOWED_COLUMNS = new Set(["name", "email", "created_at"]);

function getSortQuery(column: string, direction: string) {
  if (!ALLOWED_COLUMNS.has(column)) throw new Error("Invalid column");
  if (direction !== "asc" && direction !== "desc") throw new Error("Invalid direction");

  // Column name from allowlist, not user input
  return prisma.$queryRaw`
    SELECT * FROM "User"
    ORDER BY ${Prisma.raw(`"${column}" ${direction}`)}
  `;
}

Prisma.raw() inserts raw SQL. Only use it with validated values from an allowlist.

Reference: Prisma - Prisma.raw()

Dynamic WHERE Filters

Building dynamic filters from user input is common. Use Prisma's query builder, not string concatenation:

import { Prisma } from "@prisma/client";

function buildUserFilter(params: {
  name?: string;
  email?: string;
  role?: string;
  minAge?: number;
}): Prisma.UserWhereInput {
  const where: Prisma.UserWhereInput = {};

  if (params.name) {
    where.name = { contains: params.name, mode: "insensitive" };
  }
  if (params.email) {
    where.email = params.email;
  }
  if (params.role) {
    // Validate against known roles
    const VALID_ROLES = ["user", "admin", "moderator"];
    if (!VALID_ROLES.includes(params.role)) {
      throw new Error("Invalid role");
    }
    where.role = params.role;
  }
  if (params.minAge !== undefined) {
    where.age = { gte: params.minAge };
  }

  return where;
}

const users = await prisma.user.findMany({
  where: buildUserFilter(req.query),
});

This is safe because Prisma parameterizes all values in the generated SQL.

Dynamic IN Clauses

// SAFE - Prisma handles arrays
const users = await prisma.user.findMany({
  where: {
    id: { in: userProvidedIds }, // parameterized
  },
});

// SAFE with $queryRaw
const users = await prisma.$queryRaw`
  SELECT * FROM "User"
  WHERE id = ANY(${userProvidedIds})
`;

Pagination

Never let users control LIMIT and OFFSET without validation:

function safePagination(page?: number, pageSize?: number) {
  const safePage = Math.max(1, Math.floor(Number(page) || 1));
  const safePageSize = Math.min(100, Math.max(1, Math.floor(Number(pageSize) || 20)));

  return {
    skip: (safePage - 1) * safePageSize,
    take: safePageSize,
  };
}

const users = await prisma.user.findMany({
  ...safePagination(req.query.page, req.query.pageSize),
});

Prisma Middleware for Logging

Log all queries in development and audit sensitive queries in production:

prisma.$use(async (params, next) => {
  const start = Date.now();
  const result = await next(params);
  const duration = Date.now() - start;

  if (duration > 1000) {
    console.warn(`Slow query: ${params.model}.${params.action} took ${duration}ms`);
  }

  return result;
});

Checklist

  • Standard Prisma queries used wherever possible (auto-parameterized)
  • $queryRawUnsafe never used with user input
  • $queryRaw used with tagged template literals only
  • Column and table names validated against allowlists
  • Prisma.raw() only used with pre-validated values
  • Dynamic filters built using Prisma's query builder types
  • Pagination values validated and capped
  • Query logging enabled for performance monitoring

Related Guides

Published 04 Mar 2026

Frequently asked questions

Is Prisma safe from SQL injection by default?
The query builder is - it parameterises everything. The risk is $queryRaw and $executeRaw with template-string concatenation. Use the tagged-template form (Prisma.sql), not string interpolation.
How do I do a dynamic ORDER BY safely with Prisma?
Map the user input to an allow-list of column names server-side and pass the resulting Prisma OrderByInput object - never interpolate the column name into raw SQL.
What about full-text or LIKE searches?
Pass the user value as a parameter and let Prisma escape the % and _ characters with startsWith / contains / endsWith helpers. Avoid building LIKE patterns by string concatenation.
Do migrations need protection too?
Migrations should never include user-supplied data - they are static schema changes authored by developers. If you ever templated a migration based on runtime input, that is the bug.
Want a professional to look at it?Get an AppSec Health Check.