Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

SQL Injection no Node.js

Atualizado em: 27 de agosto de 2026

Terminal Linux usado em desenvolvimento e administração de aplicações Node.js

Evitar SQL Injection no Node.js é obrigatório em qualquer aplicação que constrói consultas a partir de dados externos. O ataque acontece quando valores fornecidos pelo usuário são interpretados como parte do comando SQL, permitindo alterar filtros, acessar registros indevidos, modificar dados ou executar operações administrativas.

A principal defesa é separar código e dados por meio de parâmetros. Validação, allowlists, permissões mínimas e testes complementam a proteção. Escapar strings manualmente ou confiar apenas em um ORM não é suficiente quando nomes de coluna, ordenação e fragmentos SQL continuam sendo concatenados.

Neste guia, você aprenderá queries parametrizadas, prepared statements, identificadores dinâmicos, filtros, LIKE, listas, ORMs, logs, privilégios, testes e defesa em profundidade.

O que é SQL Injection?

SQL Injection ocorre quando entrada não confiável modifica a estrutura da consulta. A referência da OWASP sobre SQL Injection explica o ataque e as defesas. A documentação de queries do node-postgres demonstra parâmetros seguros.

Para conexões, consulte Pool PostgreSQL no Node.js. Para uma camada adicional de isolamento, veja Row-Level Security no Node.js.

Exemplo vulnerável

const sql = `
  SELECT id, email
  FROM users
  WHERE email = '${req.query.email}'
`;

const result = await pool.query(sql);

Uma entrada contendo aspas e operadores pode alterar o WHERE.

Query parametrizada

const result = await pool.query(`
  SELECT id, email
  FROM users
  WHERE email = $1
`, [req.query.email]);

O driver envia o valor separadamente. O PostgreSQL não o interpreta como sintaxe SQL.

Parâmetros não são interpolação

Não faça:

const value = escapeSomehow(input);
const sql = `SELECT * FROM users WHERE email = '${value}'`;

Escapes manuais são frágeis diante de encoding, tipos, configurações e manutenção.

Todos os valores devem ser parâmetros

const result = await pool.query(`
  SELECT id, status, total_cents
  FROM orders
  WHERE tenant_id = $1
    AND customer_id = $2
    AND created_at >= $3
`, [tenantId, customerId, startDate]);

Identificadores não aceitam parâmetros

Placeholders não podem representar nomes de tabela ou coluna:

SELECT * FROM $1

Para ordenação dinâmica, use allowlist.

Allowlist de ordenação

const sortOptions = {
  newest: 'created_at DESC',
  oldest: 'created_at ASC',
  total_high: 'total_cents DESC',
  total_low: 'total_cents ASC'
};

const orderBy = sortOptions[req.query.sort]
  || sortOptions.newest;

const sql = `
  SELECT id, total_cents, created_at
  FROM orders
  WHERE tenant_id = $1
  ORDER BY ${orderBy}
  LIMIT $2
`;

A string usada vem apenas de valores internos predefinidos.

Nunca concatene nome recebido

ORDER BY ${req.query.column}

Mesmo que o valor pareça um nome simples, um atacante pode inserir expressão ou subconsulta.

Direção ASC ou DESC

const direction = req.query.direction === 'asc'
  ? 'ASC'
  : 'DESC';

Escolha entre duas constantes, sem reutilizar o texto recebido.

LIKE parametrizado

const term = `%${searchTerm}%`;

const result = await pool.query(`
  SELECT id, name
  FROM products
  WHERE tenant_id = $1
    AND name ILIKE $2
`, [tenantId, term]);

O parâmetro evita injection, mas caracteres % e _ continuam funcionando como curingas. Escape-os quando o requisito for busca literal.

Escapando curingas do LIKE

function escapeLike(value) {
  return value.replace(/[\\%_]/g, '\\$&');
}

const term = `%${escapeLike(searchTerm)}%`;

const result = await pool.query(`
  SELECT id, name
  FROM products
  WHERE name ILIKE $1 ESCAPE '\\'
`, [term]);

Listas com ANY

const result = await pool.query(`
  SELECT id, name
  FROM products
  WHERE tenant_id = $1
    AND id = ANY($2::uuid[])
`, [tenantId, productIds]);

Não construa uma lista separada por vírgula manualmente.

IN dinâmico

Quando a biblioteca não aceita arrays diretamente, gere placeholders com base na quantidade, nunca com base no conteúdo:

const placeholders = ids.map((_, index) =>
  `$${index + 2}`
).join(', ');

const sql = `
  SELECT id
  FROM products
  WHERE tenant_id = $1
    AND id IN (${placeholders})
`;

const values = [tenantId, ...ids];

Limite e offset

const limit = Math.min(
  Math.max(Number(req.query.limit) || 20, 1),
  100
);

const offset = Math.max(
  Number(req.query.offset) || 0,
  0
);

await pool.query(`
  SELECT id, name
  FROM products
  WHERE tenant_id = $1
  ORDER BY id
  LIMIT $2 OFFSET $3
`, [tenantId, limit, offset]);

Use inteiros validados e ainda envie como parâmetros.

Filtros opcionais

const conditions = ['tenant_id = $1'];
const values = [tenantId];

if (status) {
  values.push(status);
  conditions.push(`status = $${values.length}`);
}

if (customerId) {
  values.push(customerId);
  conditions.push(`customer_id = $${values.length}`);
}

const sql = `
  SELECT id, status
  FROM orders
  WHERE ${conditions.join(' AND ')}
`;

Os fragmentos são internos e os valores permanecem parametrizados.

Query builders

Bibliotecas como Kysely e Drizzle geram SQL parametrizado quando APIs normais são usadas. Consulte Kysely com TypeScript e SQL e Drizzle ORM com PostgreSQL.

Raw SQL em ORM

APIs de raw query podem reintroduzir o risco:

db.execute(`SELECT * FROM users WHERE email = '${email}'`);

Use o mecanismo de parâmetros da biblioteca.

Prisma

Métodos do cliente Prisma parametrizam valores. Funções unsafe ou concatenação em $queryRawUnsafe devem ser evitadas.

Prepared statements

Prepared statements também separam consulta e valores. O próximo artigo aprofunda uso, cache e limitações no PostgreSQL.

Validação ainda é necessária

Parâmetros impedem injection, mas não impedem dados inválidos. Valide UUID, enum, data, limites, tamanho e formato.

Validação não substitui parâmetros

Uma regex pode ter falha, mudar no futuro ou permitir um caso inesperado. Continue parametrizando mesmo dados validados.

Segunda ordem

Uma entrada maliciosa pode ser armazenada corretamente e depois concatenada em outra query. Dados do banco também devem ser tratados como não confiáveis quando entram em SQL dinâmico.

Stored procedures

Uma função PostgreSQL pode continuar vulnerável se construir SQL com EXECUTE e concatenação. Use format com identificadores seguros e parâmetros USING.

Busca por JSON

SELECT id
FROM events
WHERE payload @> $1::jsonb;

Serializar JSON como parâmetro é mais seguro que montar um literal.

SELECT id, title
FROM articles
WHERE search_vector @@ plainto_tsquery($1);

Prefira funções que tratam o texto como consulta, sem concatenar operadores arbitrários.

Multi-tenancy

Uma query segura contra injection ainda pode vazar dados se esquecer o tenant. Inclua o filtro e considere RLS.

Veja Multi-Tenancy no Node.js.

Permissões mínimas

A role da aplicação não deve ser superuser nem possuir DDL. Mesmo que uma falha exista, o impacto fica limitado.

Role somente leitura

Serviços de consulta podem usar uma credencial que não possui INSERT, UPDATE ou DELETE.

Separação de migration

O usuário de migration possui privilégios maiores e não deve ser usado pela API.

Statement timeout

Injection também pode tentar consultas caras. Defina timeouts e limites, embora isso não substitua a correção.

Logs

Não registre query com parâmetros sensíveis. Registre nome lógico, duração, SQLSTATE e quantidade de linhas.

Erros para o cliente

Não retorne SQL, tabela, coluna ou stack:

{
  "code": "INVALID_REQUEST",
  "message": "Não foi possível processar a solicitação"
}

Monitoramento

Observe erros de sintaxe inesperados, consultas muito lentas, padrões de aspas em parâmetros e aumento de 400 ou 500. Não bloqueie apenas por palavras-chave, pois gera falsos positivos.

WAF

Um Web Application Firewall pode reduzir tentativas conhecidas, mas não corrige concatenação no código.

Revisão de código

Procure por:

  • template strings em SQL;
  • concatenação com +;
  • APIs raw unsafe;
  • ORDER BY dinâmico;
  • nomes de tabela por input;
  • funções SECURITY DEFINER;
  • scripts administrativos;
  • queries em migrations.

Static analysis

Regras de lint e ferramentas SAST podem detectar padrões de concatenação. Configure para as bibliotecas usadas.

Testes de integração

Use um PostgreSQL real e entradas com aspas, comentários, Unicode, curingas e listas vazias.

Teste básico

test('trata texto como valor', async () => {
  const malicious = "x' OR '1'='1";

  const result = await findUserByEmail(malicious);

  assert.equal(result, null);
});

Teste de ordenação

Envie um valor fora da allowlist e confirme uso do padrão, sem erro SQL.

Teste de autorização

Mesmo com entrada maliciosa, confirme que um tenant não acessa dados de outro.

Teste de logs

Garanta que senhas, tokens e dados pessoais não aparecem quando a query falha.

Erros comuns

  • Escapar manualmente: casos especiais permanecem.
  • Parametrizar valores e concatenar ORDER BY: injection continua possível.
  • Confiar no ORM: raw queries reabrem a vulnerabilidade.
  • Validar sem parametrizar: uma mudança quebra a defesa.
  • Role com privilégios amplos: impacto aumenta.
  • Retornar erro do banco: estrutura interna é exposta.
  • Esquecer scripts internos: ferramentas administrativas também são atacáveis.

Boas práticas

  • Parametrize todos os valores.
  • Use allowlist para identificadores.
  • Valide tipos e limites.
  • Evite APIs unsafe.
  • Use role restrita.
  • Inclua tenant e RLS.
  • Defina timeouts.
  • Não exponha erros SQL.
  • Revise queries dinâmicas.
  • Teste com entradas hostis.

Conclusão

Evitar SQL Injection no Node.js começa com queries parametrizadas. Valores nunca devem ser concatenados ao SQL, e identificadores dinâmicos precisam vir de allowlists internas.

A defesa fica mais forte com validação, privilégios mínimos, RLS, timeouts e testes. ORMs e query builders ajudam, mas não eliminam o risco de raw SQL. Quando código e dados permanecem separados em todas as camadas, a aplicação reduz drasticamente a possibilidade de uma entrada externa assumir controle da consulta.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita