FNSP Web Page

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'='1

A 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.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.