Injeção de SQL: a vulnerabilidade que não devia ter sobrevivido a 1998
Tem quase trinta anos, está no topo das listas de vulnerabilidades desde que há listas, e continua a aparecer. A causa é sempre a mesma linha de código:
const sql = "SELECT * FROM utilizadores WHERE email = '" + email + "'";1. O que acontece
Se o email for [email protected], a consulta é a esperada. Se for isto:
' OR '1'='1A consulta que chega à base passa a ser:
SELECT * FROM utilizadores WHERE email = '' OR '1'='1'E devolve toda a gente. Com um pouco mais de imaginação, devolve a tabela das passwords, ou apaga-a.
O erro de fundo não é a falta de validação: é que os dados e as instruções vão pelo mesmo caminho. A base recebe um texto só e não tem como saber que parte dele era para ser lida como valor.
2. A correção: separar os dois
// A base recebe a instrução e os valores em separado.
// O que vier no email nunca é interpretado como SQL.
await db.query('SELECT * FROM utilizadores WHERE email = $1', [email]);É isto. Não é escapar aspas à mão, não é uma lista de palavras proibidas — é usar consultas parametrizadas, sempre, sem exceções para "este campo é só um número".
3. Com um ORM, quase de graça
// Parametrizado
await prisma.utilizadores.findMany({ where: { email } });
// Também parametrizado: o template literal etiquetado trata disso
await prisma.$queryRaw`SELECT * FROM utilizadores WHERE email = ${email}`;
// NÃO parametrizado. O nome do método é um aviso.
await prisma.$queryRawUnsafe(`SELECT * FROM utilizadores WHERE email = '${email}'`);A diferença entre os dois últimos é subtil e é tudo. O primeiro parece concatenação e não é: a etiqueta antes da crase recebe os valores à parte. O segundo é concatenação a sério.
4. Onde ainda escapa
Os parâmetros substituem valores. Não substituem nomes de tabelas nem de colunas:
// Isto não funciona, e é por design
db.query('SELECT * FROM utilizadores ORDER BY $1', [coluna]);Quando a coluna a ordenar vem do exterior — um clique num cabeçalho de tabela — a única defesa é uma lista fechada:
const PERMITIDAS = { nome: 'nome', data: 'criado_em' };
const coluna = PERMITIDAS[pedido.ordenarPor] ?? 'criado_em';Escolher de uma lista que você escreveu, e nunca aceitar o que vem de fora.