Injeção de SQL: como funciona e como prevenir

1. O que é Injeção de SQL (SQLi)

Injeção de SQL (SQL Injection ou SQLi) é uma vulnerabilidade de segurança que ocorre quando um atacante consegue inserir ou "injetar" comandos SQL maliciosos em uma consulta, através de dados fornecidos pelo usuário. Esta vulnerabilidade está consistentemente presente no OWASP Top 10, sendo uma das ameaças mais críticas para aplicações web.

A falha surge quando o código da aplicação concatena diretamente entradas do usuário em consultas SQL, sem qualquer validação ou sanitização adequada. O banco de dados interpreta o input malicioso como parte do comando SQL, permitindo que o atacante execute operações não autorizadas.

Exemplo de consulta vulnerável:

consulta_sql = "SELECT * FROM usuarios WHERE email = '" + email_usuario + "' AND senha = '" + senha_usuario + "'"

2. Tipos de Injeção de SQL

In-Band SQLi

É o tipo mais comum, onde o atacante utiliza o mesmo canal de comunicação para injetar o código e receber os resultados. Divide-se em duas variações:

  • Baseada em erro: O atacante força o banco a gerar mensagens de erro que revelam informações sobre a estrutura do banco.
  • Baseada em UNION: Utiliza o operador UNION para combinar resultados da consulta original com dados de outras tabelas.

Blind SQLi

Ocorre quando a aplicação não exibe dados diretamente, mas o atacante pode inferir informações através de respostas booleanas (verdadeiro/falso) ou atrasos temporários na resposta.

  • Booleana: Pergunta-se ao banco "sim ou não" através de condições que alteram o comportamento da página.
  • Baseada em tempo: Usa funções como SLEEP() para causar atrasos e inferir resultados.

Out-of-Band SQLi

Utiliza canais alternativos (DNS, HTTP requests) para exfiltrar dados, geralmente quando o banco possui capacidades limitadas de resposta ou quando firewalls bloqueiam respostas diretas.

3. Como a Injeção de SQL Funciona na Prática

O princípio básico é manipular a estrutura da consulta SQL original, quebrando aspas e inserindo comandos maliciosos. O uso de comentários SQL (-- ou /* */) permite descartar o restante da consulta original.

Exemplo de ataque de login bypass:

Supondo a consulta original:
SELECT * FROM usuarios WHERE email = 'admin@exemplo.com' AND senha = 'senha123'

Payload malicioso inserido no campo email:
admin@exemplo.com' OR '1'='1' --

Consulta resultante:
SELECT * FROM usuarios WHERE email = 'admin@exemplo.com' OR '1'='1' --' AND senha = 'qualquercoisa'

O OR '1'='1' torna a condição sempre verdadeira, e o -- comenta o restante da consulta, ignorando a verificação de senha. O atacante obtém acesso como o primeiro usuário da tabela (geralmente um administrador).

Para extrair dados de outras tabelas, utiliza-se UNION:

' UNION SELECT id, nome, 'admin' FROM administradores --

4. Consequências de um Ataque SQLi

Um ataque SQLi bem-sucedido pode ter consequências devastadoras:

  • Acesso não autorizado: Roubo de credenciais de usuários, dados pessoais, informações financeiras e segredos comerciais.
  • Modificação ou exclusão de dados: Perda de integridade do banco, corrupção de registros ou exclusão completa de tabelas.
  • Escalação de privilégios: O atacante pode obter acesso administrativo ao banco e, em alguns casos, executar comandos no sistema operacional do servidor.
  • Comprometimento total: Em cenários extremos, o atacante pode assumir o controle completo do servidor de banco de dados.

5. Prevenção: Uso de Prepared Statements (Consultas Parametrizadas)

A defesa mais eficaz contra SQLi é o uso de prepared statements (consultas parametrizadas). Esta técnica separa completamente o código SQL dos dados fornecidos pelo usuário. O banco de dados compila primeiro a estrutura da consulta e depois insere os parâmetros como dados, nunca como parte executável do comando.

Exemplo seguro em PHP com PDO:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=meubanco', 'usuario', 'senha');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$email = $_POST['email'];
$senha = $_POST['senha'];

$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = :email AND senha = :senha');
$stmt->execute(['email' => $email, 'senha' => $senha]);
$usuario = $stmt->fetch();
?>

Exemplo seguro em Python com sqlite3:

import sqlite3

conexao = sqlite3.connect('meubanco.db')
cursor = conexao.cursor()

email = input("Digite o email: ")
senha = input("Digite a senha: ")

cursor.execute("SELECT * FROM usuarios WHERE email = ? AND senha = ?", (email, senha))
usuario = cursor.fetchone()

Nos exemplos acima, mesmo que o usuário forneça ' OR '1'='1' --, o banco tratará isso como uma string literal, não como código SQL. O prepared statement garante que os parâmetros nunca sejam interpretados como comandos.

6. Prevenção: Boas Práticas Adicionais

Validação e Sanitização de Entradas

  • Utilize whitelist (lista de valores permitidos) em vez de blacklist (tentar bloquear valores maliciosos).
  • Valide tipos de dados (números devem ser inteiros, emails devem seguir formato válido).
  • Limite o tamanho máximo dos campos.

Stored Procedures com Parâmetros Seguros

Stored procedures também podem ser seguras se implementadas corretamente com parâmetros, mas é importante garantir que a procedure em si não construa consultas dinâmicas concatenadas.

Princípio do Menor Privilégio

  • Crie usuários de banco com permissões mínimas necessárias para a aplicação.
  • Evite usar contas com privilégios administrativos (como root ou sa) para operações normais.
  • Restrinja acesso a stored procedures e views em vez de tabelas diretamente.

7. Ferramentas e Testes para Detecção de SQLi

Ferramentas Automatizadas

  • SQLMap: Ferramenta open source que automatiza detecção e exploração de SQLi.
  • OWASP ZAP: Proxy de segurança que inclui scanners automáticos para SQLi.
  • Burp Suite: Plataforma completa para testes de segurança web com módulo de scanner.

Testes Manuais

  • Envie payloads comuns como ', ", ;, -- e observe erros ou comportamentos anormais.
  • Teste variações de codificação (URL encoding, Unicode) para contornar filtros.
  • Analise respostas para diferenças entre condições verdadeiras e falsas.

Integração Contínua

  • Incorpore ferramentas de Análise Estática de Segurança (SAST) no pipeline CI/CD.
  • Configure scanners automáticos para testar endpoints de API e formulários.
  • Realize testes de penetração regulares em ambientes de homologação.

8. Exemplo Completo: Do Código Vulnerável ao Seguro

Código Vulnerável (PHP)

<?php
$conexao = mysqli_connect("localhost", "root", "", "loja");

$produto_id = $_GET['id'];

$sql = "SELECT nome, preco FROM produtos WHERE id = " . $produto_id;
$resultado = mysqli_query($conexao, $sql);

if ($row = mysqli_fetch_assoc($resultado)) {
    echo "Produto: " . $row['nome'] . " - Preço: R$ " . $row['preco'];
} else {
    echo "Produto não encontrado.";
}
?>

Problema: O parâmetro id é concatenado diretamente na consulta. O atacante pode enviar 1 UNION SELECT senha, email FROM administradores e obter dados sensíveis.

Código Seguro (PHP com PDO)

<?php
$pdo = new PDO('mysql:host=localhost;dbname=loja', 'usuario_loja', 'senha_segura');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$produto_id = $_GET['id'];

// Validação adicional: garantir que é um número inteiro
if (!ctype_digit($produto_id)) {
    die("ID inválido.");
}

$stmt = $pdo->prepare("SELECT nome, preco FROM produtos WHERE id = :id");
$stmt->execute(['id' => (int)$produto_id]);
$produto = $stmt->fetch(PDO::FETCH_ASSOC);

if ($produto) {
    echo "Produto: " . htmlspecialchars($produto['nome']) . " - Preço: R$ " . number_format($produto['preco'], 2, ',', '.');
} else {
    echo "Produto não encontrado.";
}
?>

Melhorias:
1. Uso de prepared statement com placeholder :id.
2. Validação de tipo com ctype_digit() e cast para inteiro.
3. Conexão com usuário de privilégios limitados (usuario_loja).
4. Sanitização da saída com htmlspecialchars() para prevenir XSS.

Comparação de Comportamento

Entrada Código Vulnerável Código Seguro
1 Produto encontrado Produto encontrado
1 UNION SELECT senha, email FROM administradores Dados dos administradores exibidos "ID inválido" ou produto não encontrado
1; DROP TABLE produtos; -- Tabela produtos excluída "ID inválido" (validação rejeita)

Lição final: A prevenção de SQLi não depende de um único mecanismo, mas de uma combinação de prepared statements, validação rigorosa de entradas, princípio do menor privilégio e testes contínuos. Nunca confie em dados fornecidos pelo usuário. Trate toda entrada como potencialmente maliciosa até que seja provado o contrário.

Referências