flawopen.com/sql-injection/Javascript
SQL Injection em JavaScript (mysql2, pg, Prisma, Knex)
Um template literal ou + dentro de pool.query() permite que um parâmetro da requisição reescreva o seu SQL e devolva todas as linhas (CWE-89). A chamada segura em mysql2, node-postgres, Prisma e Knex, por que ${} é seguro num método do Prisma e perigoso no seguinte, e como ordenar sem orderByRaw.[1], [2], [3], [4], [5], [6]
Imagine um centro de distribuição: a rota é impressa e travada antes de qualquer pacote chegar. O nome na etiqueta ocupa um campo próprio; não pode acrescentar uma parada à rota. Parâmetros SQL funcionam assim: mysql2 execute(sql, [userId]) e pg query(sql, [status]) vinculam valores, enquanto o $queryRaw com tag do Prisma vincula cada espaço ${}. Se você montar o SQL antes com template literal ou +, ou usar $queryRawUnsafe, o texto do cliente é escrito na própria rota. Knex orderByRaw tem o mesmo risco para instruções de ordenação; associe a chave pedida a uma coluna fixa e chame orderBy.[1], [2], [3], [4], [5], [6]
Conceitos Centrais e Termos
Template literals in query()- Uma string entre crases com
${userId}é avaliada pelo JavaScript antes de o driver ser chamado, então o driver recebe um SQL pronto com o valor já dentro. A sintaxe parece um placeholder, mas não vincula nada.[7], [1], [6] mysql2 execute() vs query()execute(sql, values)usa um prepared statement no servidor.query(sql, values)escapa os valores no cliente e os substitui nos marcadores?. Os dois são seguros quando os valores estão no array; nenhum ajuda se a string SQL foi montada com+ou template literal. O mysql2 desativamultipleStatementspor padrão.[3], [6], [9]pg query(text, values)- O node-postgres envia
valuesseparado detext, então quem preenche$1é o PostgreSQL. Sem valores, o pg usa o protocolo de consulta simples, que também aceita vários comandos separados por ponto e vírgula numa chamada.[2], [6] Prisma $queryRaw vs $queryRawUnsafe$queryRawé um tagged template: o Prisma transforma cada${}em parâmetro vinculado.$queryRawUnsafe(string)recebe uma string pronta, então o mesmo${}dentro de um template literal comum é colado no SQL. Os nomes diferem por uma palavra e o risco é o oposto.[4], [6]
Mapeamentos de frameworks
- Referência CWE para a fraqueza descrita neste guia: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Fluxo de Ataque Passo a Paso
Código-Fonte: Vulnerável vs. Seguro
// users.js: cada consulta abaixo monta texto SQL com dados da requisição
// mysql2: o template literal é avaliado antes de o driver ser chamado
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg: concatenação, e nenhum array de valores para vincular
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma: a variante Unsafe recebe uma string pronta
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex: orderByRaw insere o valor da requisição como SQL cru
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
// users.js: valores vão no array de valores, colunas vêm de uma allowlist
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2: prepared statement no servidor, valor vinculado ao ?
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg: o PostgreSQL preenche $1 a partir do 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 ${} vira um parâmetro vinculado
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex: orderBy põe a coluna da allowlist entre aspas como identificador
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
Lista de Verificação de Segurança para Engenharia
- ✓Proíba
$queryRawUnsafe,$executeRawUnsafe,whereRaw/orderByRawcom interpolação e template literals ou+no primeiro argumento dequery()/execute()com uma regra ESLintno-restricted-syntaxou Semgrep na CI.[1], [3], [2], [4], [5], [6] - ✓Passe todo valor no array de valores do driver (
?no mysql2,$1no pg) ou como${}no$queryRawcom tag do Prisma.[3], [2], [4], [6] - ✓Valide a entrada com um schema (zod, Joi) na rota: IDs viram números, chaves desconhecidas são rejeitadas, arrays não são aceitos onde se espera uma string.[8], [6]
- ✓Mapeie as chaves de ordenação por uma allowlist
Mape passe o resultado aoorderBy()do Knex ou aoorderBydo Prisma; nunca passereq.querya um ORDER BY cru.[5], [4], [6] - ✓Mantenha
multipleStatementsdo mysql2 desligado e conecte com um usuário de banco sem direitos de DDL.[3], [9], [6]
Fontes
- 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