flawopen.com/sql-injection/Javascript
Inyección SQL en JavaScript (mysql2, pg, Prisma, Knex)
Un template literal o + dentro de pool.query() permite que un parámetro de la petición reescriba tu SQL y devuelva todas las filas (CWE-89). La llamada segura en mysql2, node-postgres, Prisma y Knex, por qué ${} es seguro en un método de Prisma y peligroso en el siguiente, y cómo ordenar sin orderByRaw.[1], [2], [3], [4], [5], [6]
Imagina un centro de paquetería: la ruta se imprime y queda cerrada antes de que llegue ningún paquete. El nombre de la etiqueta ocupa un campo propio; no puede añadir una parada a la ruta. Los parámetros SQL funcionan igual: mysql2 execute(sql, [userId]) y pg query(sql, [status]) vinculan valores, y el $queryRaw etiquetado de Prisma vincula cada hueco ${}. Si primero montas el SQL con un template literal o +, o usas $queryRawUnsafe, el texto del cliente acaba escrito en la ruta. Knex orderByRaw presenta el mismo riesgo con la ordenación; asigna la clave solicitada a una columna fija y llama a orderBy.[1], [2], [3], [4], [5], [6]
Conceptos Clave y Términos
Template literals in query()- Una cadena entre comillas invertidas con
${userId}la evalúa JavaScript antes de llamar al driver, así que el driver recibe un SQL terminado con el valor ya dentro. La sintaxis parece un marcador, pero no enlaza nada.[7], [1], [6] mysql2 execute() vs query()execute(sql, values)usa una sentencia preparada en el servidor.query(sql, values)escapa los valores en el cliente y los sustituye en los marcadores?. Ambos son seguros si los valores van en el array; ninguno sirve si la cadena SQL se construyó con+o un template literal. mysql2 desactivamultipleStatementspor defecto.[3], [6], [9]pg query(text, values)- node-postgres envía
valuespor separado detext, así que$1lo rellena PostgreSQL. Sin valores, pg usa el protocolo de consulta simple, que además acepta varias sentencias separadas por punto y coma en una llamada.[2], [6] Prisma $queryRaw vs $queryRawUnsafe$queryRawes un tagged template: Prisma convierte cada${}en un parámetro enlazado.$queryRawUnsafe(string)recibe una cadena ya hecha, así que el mismo${}dentro de un template literal normal se pega en el SQL. Los nombres difieren en una palabra y el riesgo es el contrario.[4], [6]
Mapeos de marcos
- Referencia CWE para la debilidad descrita en esta guía: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Flujo de Ataque Paso a Paso
Código Fuente: Vulnerable vs. Seguro
// users.js: cada consulta de abajo construye texto SQL con datos de la petición
// mysql2: el template literal se evalúa antes de llamar al driver
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg: concatenación, y ningún array de valores que enlazar
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma: la variante Unsafe recibe una cadena ya hecha
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex: orderByRaw inserta el valor de la petición como SQL en bruto
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
// users.js: los valores van en el array de valores, las columnas salen de una lista permitida
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2: sentencia preparada en el servidor, valor enlazado al ?
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg: PostgreSQL rellena $1 con el array de valores
export async function findOrders(pg, status) {
const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
return rows;
}
// Prisma: tagged template, cada ${} se convierte en un parámetro enlazado
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex: orderBy entrecomilla la columna permitida como identificador
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
Lista de Verificación de Seguridad para Ingeniería
- ✓Prohíbe
$queryRawUnsafe,$executeRawUnsafe,whereRaw/orderByRawcon interpolación y los template literals o+en el primer argumento dequery()/execute()con una regla ESLintno-restricted-syntaxo con Semgrep en la CI.[1], [3], [2], [4], [5], [6] - ✓Pasa cada valor en el array de valores del driver (
?en mysql2,$1en pg) o como${}en el$queryRawetiquetado de Prisma.[3], [2], [4], [6] - ✓Valida la entrada con un esquema (zod, Joi) en la ruta: los IDs pasan a ser números, se rechazan claves desconocidas y no se aceptan arrays donde se espera una cadena.[8], [6]
- ✓Pasa las claves de orden por una lista permitida
Mapy entrega el resultado aorderBy()de Knex o alorderByde Prisma; nunca pasesreq.querya un ORDER BY en bruto.[5], [4], [6] - ✓Mantén
multipleStatementsde mysql2 desactivado y conéctate con un usuario de base de datos sin permisos de DDL.[3], [9], [6]
Fuentes
- 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