Prepared statements e prevenção de SQL injection
1. O Perigo do SQL Injection
SQL Injection é uma das vulnerabilidades mais críticas e persistentes em aplicações web. Ela ocorre quando um atacante consegue inserir código SQL malicioso através de campos de entrada do usuário, como formulários, URLs ou cookies. O ataque explora a falta de sanitização adequada dos dados antes de serem incluídos em consultas SQL.
Exemplo clássico de vulnerabilidade:
// Código VULNERÁVEL - NUNCA faça isso!
$email = $_POST['email'];
$senha = $_POST['senha'];
$sql = "SELECT * FROM usuarios WHERE email = '$email' AND senha = '$senha'";
$resultado = mysqli_query($conexao, $sql);
Se um atacante enviar ' OR '1'='1 como email e ' OR '1'='1 como senha, a consulta se torna:
SELECT * FROM usuarios WHERE email = '' OR '1'='1' AND senha = '' OR '1'='1'
Isso retornaria todos os usuários do banco, permitindo acesso não autorizado.
Consequências reais:
- Roubo de dados sensíveis (senhas, dados financeiros)
- Alteração ou exclusão de registros
- Comprometimento total do servidor
- Instalação de malware
O PHP é um alvo comum por causa de:
- Grande quantidade de código legado sem práticas modernas
- Facilidade de aprendizado que leva a más práticas
- Tutoriais antigos que ainda ensinam concatenação direta
2. Entendendo Prepared Statements
Prepared statements (declarações preparadas) resolvem o problema do SQL Injection separando completamente a estrutura SQL dos dados fornecidos pelo usuário.
Ciclo de funcionamento:
1. Preparar: O banco de dados analisa e compila a estrutura SQL
2. Vincular parâmetros: Os placeholders são substituídos pelos valores reais
3. Executar: A consulta completa é executada
-- Estrutura SQL com placeholder
INSERT INTO usuarios (nome, email) VALUES (?, ?)
-- O banco entende que ? serão dados, nunca código
Diferença fundamental:
- Consulta estática: SQL fixo, sem variáveis do usuário
- Consulta dinâmica: Precisa de dados do usuário → use prepared statements
3. Prepared Statements com MySQLi
MySQLi oferece suporte nativo a prepared statements através de funções específicas.
Exemplo de INSERT:
$conexao = new mysqli("localhost", "usuario", "senha", "banco");
// 1. Preparar
$stmt = $conexao->prepare("INSERT INTO usuarios (nome, email, idade) VALUES (?, ?, ?)");
// 2. Vincular parâmetros (s=string, s=string, i=integer)
$stmt->bind_param("ssi", $nome, $email, $idade);
// 3. Atribuir valores
$nome = "Maria Silva";
$email = "maria@email.com";
$idade = 28;
// 4. Executar
$stmt->execute();
echo "Usuário inserido com ID: " . $stmt->insert_id;
$stmt->close();
Tipos de parâmetros:
- s: string
- i: integer
- d: double/float
- b: blob (dados binários)
Exemplo de SELECT com condição:
$stmt = $conexao->prepare("SELECT * FROM produtos WHERE preco < ? AND categoria = ?");
$stmt->bind_param("ds", $precoMaximo, $categoria);
$precoMaximo = 50.00;
$categoria = "eletrônicos";
$stmt->execute();
$resultado = $stmt->get_result();
while ($produto = $resultado->fetch_assoc()) {
echo $produto['nome'] . " - R$ " . $produto['preco'] . "<br>";
}
$stmt->close();
Exemplo de UPDATE:
$stmt = $conexao->prepare("UPDATE usuarios SET ultimo_login = NOW() WHERE id = ?");
$stmt->bind_param("i", $usuarioId);
$usuarioId = 42;
$stmt->execute();
echo "Linhas afetadas: " . $stmt->affected_rows;
$stmt->close();
4. Prepared Statements com PDO
PDO (PHP Data Objects) oferece uma interface mais moderna e flexível, suportando múltiplos bancos de dados.
Configuração inicial com tratamento de erros:
try {
$pdo = new PDO("mysql:host=localhost;dbname=banco;charset=utf8mb4", "usuario", "senha");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Erro de conexão: " . $e->getMessage());
}
Placeholders nomeados vs posicionais:
// Placeholders nomeados (:nome)
$stmt = $pdo->prepare("INSERT INTO usuarios (nome, email, idade) VALUES (:nome, :email, :idade)");
$stmt->execute([
':nome' => 'João',
':email' => 'joao@email.com',
':idade' => 35
]);
// Placeholders posicionais (?)
$stmt = $pdo->prepare("INSERT INTO usuarios (nome, email, idade) VALUES (?, ?, ?)");
$stmt->execute(['Maria', 'maria@email.com', 28]);
bindParam() vs bindValue() vs array na execução:
// bindParam() - vincula por referência
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE idade > ?");
$stmt->bindParam(1, $idadeMinima, PDO::PARAM_INT);
$idadeMinima = 18;
$stmt->execute(); // Usa o valor atual de $idadeMinima
// bindValue() - vincula por valor
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE idade > ?");
$stmt->bindValue(1, 18, PDO::PARAM_INT);
// Mesmo que $idadeMinima mude depois, a consulta usa 18
// Array na execução (mais conciso)
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE idade > ? AND ativo = ?");
$stmt->execute([18, 1]);
5. Prevenção Avançada: Além do Básico
Cuidados com LIKE: Prepared statements protegem contra injeção, mas caracteres curinga ainda funcionam.
// Perigoso: usuário pode enviar % para buscar tudo
$busca = "%"; // Isso retornaria todos os registros
$stmt = $pdo->prepare("SELECT * FROM produtos WHERE nome LIKE ?");
$stmt->execute(["%$busca%"]);
// Correto: escapar caracteres curinga
$busca = "%";
$buscaSegura = addcslashes($busca, '%_');
$stmt = $pdo->prepare("SELECT * FROM produtos WHERE nome LIKE ?");
$stmt->execute(["%$buscaSegura%"]);
ORDER BY e nomes de colunas: Prepared statements não podem ser usados para nomes de colunas ou tabelas. Use whitelist:
function orderBySeguro($campo, $direcao = 'ASC') {
$camposPermitidos = ['nome', 'preco', 'data_criacao'];
$direcoesPermitidas = ['ASC', 'DESC'];
if (!in_array($campo, $camposPermitidos)) {
$campo = 'nome'; // valor padrão seguro
}
if (!in_array(strtoupper($direcao), $direcoesPermitidas)) {
$direcao = 'ASC';
}
return "ORDER BY $campo $direcao";
}
$sql = "SELECT * FROM produtos " . orderBySeguro($_GET['ordenar'], $_GET['dir']);
$stmt = $pdo->query($sql);
6. Mitos e Erros Comuns
Mito 1: mysqli_real_escape_string() substitui prepared statements
// AINDA é vulnerável em alguns casos
$nome = mysqli_real_escape_string($conexao, $_POST['nome']);
$sql = "SELECT * FROM usuarios WHERE nome = '$nome'"; // Inseguro!
// Prepared statement é SEMPRE superior
$stmt = $conexao->prepare("SELECT * FROM usuarios WHERE nome = ?");
$stmt->bind_param("s", $_POST['nome']);
$stmt->execute();
Mito 2: Prepared statements são apenas para consultas em cache
Prepared statements são sobre segurança, não performance. Embora alguns bancos armazenem em cache, o principal benefício é a prevenção de injeção.
Erro comum: Concatenar mesmo com prepared statements
// ERRADO: anula a proteção
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE email = '" . $email . "'");
$stmt->execute();
// CORRETO
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE email = ?");
$stmt->execute([$email]);
7. Boas Práticas Integradas
Validação de dados no PHP + prepared statements:
function cadastrarUsuario($dados) {
// Validação no PHP
if (!filter_var($dados['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Email inválido");
}
if (strlen($dados['senha']) < 8) {
throw new InvalidArgumentException("Senha muito curta");
}
// Prepared statement para inserção segura
$stmt = $pdo->prepare("INSERT INTO usuarios (email, senha_hash) VALUES (?, ?)");
$stmt->execute([
$dados['email'],
password_hash($dados['senha'], PASSWORD_BCRYPT)
]);
}
Uso de transações com PDO:
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO pedidos (usuario_id, total) VALUES (?, ?)");
$stmt->execute([$usuarioId, $total]);
$pedidoId = $pdo->lastInsertId();
$stmt = $pdo->prepare("INSERT INTO itens_pedido (pedido_id, produto_id, quantidade) VALUES (?, ?, ?)");
foreach ($itens as $item) {
$stmt->execute([$pedidoId, $item['produto_id'], $item['quantidade']]);
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
error_log("Erro no pedido: " . $e->getMessage());
throw $e;
}
Logging de tentativas suspeitas:
function detectarTentativaInjecao($entrada) {
$padroesSuspeitos = ['SELECT.*FROM', 'DROP\s+TABLE', 'UNION\s+SELECT', '--', ';\s*DROP'];
foreach ($padroesSuspeitos as $padrao) {
if (preg_match("/$padrao/i", $entrada)) {
error_log("Tentativa de SQL Injection detectada: " . $_SERVER['REMOTE_ADDR']);
return true;
}
}
return false;
}
O uso consistente de prepared statements combinado com validação adequada, transações e monitoramento forma uma defesa robusta contra SQL Injection. Lembre-se: a segurança nunca deve ser uma reflexão tardia no desenvolvimento PHP.
Referências
- PHP Manual: Prepared Statements and Stored Procedures — Documentação oficial do PHP sobre prepared statements com MySQLi
- PHP Manual: PDO Prepared Statements — Referência completa sobre prepared statements usando PDO
- OWASP: SQL Injection Prevention Cheat Sheet — Guia oficial da OWASP com as melhores práticas de prevenção
- PHP The Right Way: Database Interaction — Guia moderno de boas práticas PHP, incluindo prepared statements
- PortSwigger: SQL Injection Cheat Sheet — Referência técnica sobre técnicas de SQL Injection e como se proteger
- MySQL Documentation: Prepared Statements — Documentação oficial do MySQL sobre o funcionamento interno dos prepared statements