Prevention — Node.js
Placeholders in pg and mysql2, Knex raw bindings, and Prisma's tagged-template-vs-Unsafe distinction.
The Rule
Use placeholders ($1, ?) with the driver's parameter array, or a query builder. Never build SQL with template literals containing user input.
The Node-specific trap is template literals: they look like the natural way to build a string, and they are exactly the wrong tool. The exception is Prisma's $queryRaw, which is a tagged template that parameterises each ${} — but only if you do not defeat it by pre-building the string.
node-postgres (pg)
// Template literal with user input. Injectable.
const { rows } = await client.query(
`SELECT * FROM users WHERE name = '${name}'`
)
// Concatenation, same problem
const r = await client.query(
"SELECT * FROM users WHERE id = " + id)mysql2
const [rows] = await conn.query(
`SELECT * FROM users WHERE name = '${name}'`)Knex
// Template literal inside raw(), and string concatenation
// in whereRaw, are both injectable.
knex.raw(`SELECT * FROM users WHERE name = '${name}'`)
knex('users').whereRaw("name = '" + name + "'")
knex('products').orderByRaw(sort) // identifier contextPrisma
// $queryRawUnsafe takes a plain string.
await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${email}'`)
// Defeating the tagged template by pre-building the string:
const q = `SELECT * FROM users WHERE email = '${email}'`
await prisma.$queryRaw(Prisma.raw(q)) // one interpolation = whole queryChecklist
- Placeholders (
$1,?) with a values array; never a template literal containing user input. - pg/mysql2: prefer
execute()(real prepares) overquery(). - Knex: builder methods;
?/??bindings for raw; allowlist for order. - Prisma:
$queryRawtagged template with bare${}; ban$queryRawUnsafeandPrisma.raw(builtString)in review. - NoSQL/Mongo: validate types at the boundary — see NoSQL Injection.
- Identifiers: allowlist.
- Add an ESLint rule or Semgrep to flag
$queryRawUnsafeand template literals in.query(...). - Least-privilege database role — see PostgreSQL.
Related
Every ORM has an escape hatch to raw SQL, and every one of them is a footgun. Where the abstraction stops protecting you, and which method names to grep for.
The same trust failure without the SQL. Operator injection in MongoDB, server-side JavaScript execution, and why 'we don't use SQL' is not a defence.
Filter, sort and search parameters in REST and GraphQL are built for flexibility — which usually means built by concatenation. Where modern APIs reintroduce a solved problem.
Strict typing, stacked queries, and COPY TO PROGRAM — the engine where an injection most reliably becomes command execution.
Parameterisation removes the bug; these layers limit the blast radius when a parameterisation is missed. Least privilege, allowlists, monitoring, and what each is actually worth.