6 min read

SQL injection prevention in Node.js — parametrize everything

SQL injection is a solved problem — and AI coding assistants keep re-introducing it. Here are the exact patterns to watch for in Prisma, Supabase, Drizzle, and raw pg.

Every ORM supports parameterized queries. AI-generated code sometimes reaches for raw SQL anyway — to ship faster, to match a copy-pasted example. This guide covers the exact patterns that slip through code review.

What it is

SQL injection happens when user input is interpolated into a SQL string. The fix is parameter binding, where the database receives SQL and values as separate inputs and never compiles user input as SQL.

Vulnerable example

// Vulnerable in every ORM:
// 1. Raw pg
const result = await pool.query(`SELECT * FROM users WHERE email = '${email}'`);

// 2. Prisma raw query
const users = await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);

// 3. Drizzle sql template with interpolation
const users = await db.execute(sql`${sql.raw(`SELECT * FROM users WHERE email = '${email}'`)}`);

Fixed example

// Fixed: parameter binding
// 1. Raw pg
const result = await pool.query("SELECT * FROM users WHERE email = $1", [email]);

// 2. Prisma typed
const users = await prisma.user.findMany({ where: { email } });

// 3. Prisma raw with bindings
const users = await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;

// 4. Drizzle typed
const users = await db.select().from(usersTable).where(eq(usersTable.email, email));

How Securie catches it

Securie findingcritical
apps/web/app/api/route.ts:22

SQL injection prevention in Node.js

Securie's taint analyzer traces every user input into every SQL sink across the most common Node ORMs, flagging any string interpolation that reaches SQL.

Suggested fix — ready as a PR
// Fixed: parameter binding
// 1. Raw pg
const result = await pool.query("SELECT * FROM users WHERE email = $1", [email]);

// 2. Prisma typed
const users = await prisma.user.findMany({ where: { email } });

// 3. Prisma raw with bindings
const users = await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;

// 4. Drizzle typed
const users = await db.select().from(usersTable).where(eq(usersTable.email, email));
Catch this in my repo →Securie reviews every PR · proves the issue · ships a verified fix PR

Checklist

  • Zero string concatenation in SQL
  • Raw query APIs (`$queryRawUnsafe`, `pool.query` with template strings) are grep'd for and each use is reviewed
  • LIKE clauses use parameterized %s with explicit escape
  • Dynamic identifiers (table/column names from user input) are whitelisted, not concatenated
  • Database users have read-only where possible; prod DB cannot DROP

FAQ

What about NoSQL injection?

Same principle. MongoDB drivers bind parameters correctly; `$where` with interpolation is the equivalent anti-pattern. Securie covers MongoDB too.

Related guides