flawopen.com/sql-injection/Javascript
SQL Injection in JavaScript (mysql2, pg, Prisma, Knex)
A template literal or + inside pool.query() lets a request parameter rewrite your SQL and return every row (CWE-89). The safe call for mysql2, node-postgres, Prisma and Knex, why ${} is safe in one Prisma method and dangerous in the next, and how to sort without orderByRaw.[1], [2], [3], [4], [5], [6]
Picture a parcel hub: the route sheet is printed and locked before any box arrives. A name on a label goes into the named slot; it cannot add a new stop to the route. SQL parameters work the same way: mysql2 execute(sql, [userId]) and pg query(sql, [status]) bind values, while Prisma's tagged $queryRaw binds each ${} hole. Build SQL first with a template literal or +, or use $queryRawUnsafe, and the customer's text is written into the route sheet itself. Knex orderByRaw has the same problem for sort instructions; map a requested key to a fixed column and call orderBy.[1], [2], [3], [4], [5], [6]
Core Concepts & Subsystem Terms
Template literals in query()- A backtick string with
${userId}is evaluated by JavaScript before the driver is called, so the driver receives finished SQL with the value already inside. The syntax looks like a placeholder but binds nothing.[7], [1], [6] mysql2 execute() vs query()execute(sql, values)uses a server-side prepared statement.query(sql, values)escapes values on the client and substitutes them into?markers. Both are safe when values are in the array; neither helps when the SQL string was built with+or a template literal. mysql2 disablesmultipleStatementsby default.[3], [6], [9]pg query(text, values)- node-postgres sends
valuesseparately fromtext, so$1is filled in by PostgreSQL. With no values, pg uses the simple query protocol, which also accepts several semicolon-separated statements in one call.[2], [6]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Source Code: Flaw vs. Secure Implementation
// users.js: each query below builds SQL text from request data
// mysql2: the template literal is evaluated before the driver is called
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg: concatenation, and no values array to bind
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma: the Unsafe variant takes a finished string
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex: orderByRaw inserts the request value as raw SQL
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
// users.js: values go in the values array, columns come from an allowlist
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2: server-side prepared statement, value bound to ?
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg: $1 filled in by PostgreSQL from the values array
export async function findOrders(pg, status) {
const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
return rows;
}
// Prisma: tagged template, each ${} becomes a bound parameter
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex: orderBy quotes the allowlisted column as an identifier
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
Engineering & System Hardening Checklist
- ✓Ban
$queryRawUnsafe,$executeRawUnsafe,whereRaw/orderByRawwith interpolation, and template literals or+in the first argument ofquery()/execute()with an ESLintno-restricted-syntaxrule or Semgrep in CI.[1], [3], [2], [4], [5], [6] - ✓Pass every value in the driver's values array (
?for mysql2,$1for pg) or as${}in Prisma's tagged$queryRaw.[3], [2], [4], [6] - ✓Validate request input with a schema (zod, Joi) at the route: IDs become numbers, unknown keys are rejected, arrays are not accepted where a string is expected.[8], [6]
- ✓Map sort keys through a
Mapallowlist and pass the result to KnexorderBy()or PrismaorderBy; never passreq.queryto a raw ORDER BY.[5], [4], [6] - ✓Keep mysql2's
multipleStatementsoff and connect with a database user that has no DDL rights.[3], [9], [6]
Sources
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- node-postgres — Parameterized query
- mysql2 — Using prepared statements
- Prisma — Raw queries and SQL injection
- Knex.js — Raw parameter binding
- OWASP SQL Injection Prevention Cheat Sheet
- MDN — Template literals (JavaScript)
- Express 4 API — req.query
- MySQL C API — Multiple statement execution support