Prevenindo SQL injection
Esta aula explica o que é SQL injection, como prepared statements protegem contra esse ataque, erros comuns ao tentar prevenir e boas práticas para escrever código seguro em PHP.
SQL injection é um dos ataques mais comuns e perigosos em aplicações web. Ele ocorre quando dados fornecidos pelo usuário são inseridos diretamente em uma consulta SQL sem a devida sanitização, permitindo que um invasor execute comandos maliciosos no banco de dados. Nesta aula, você aprenderá a prevenir esse ataque utilizando prepared statements e adotando boas práticas de segurança.
O PHP oferece suporte nativo a prepared statements através da extensão PDO (PHP Data Objects) e da extensão MySQLi. Ambas permitem separar a estrutura da consulta dos dados, eliminando a possibilidade de injeção. Vamos focar no uso de PDO por ser mais flexível e compatível com diversos bancos de dados.
O que é SQL injection
SQL injection é uma técnica de ataque onde o invasor insere código SQL malicioso através de campos de entrada, como formulários de login, campos de busca ou URLs. Se a aplicação construir consultas SQL concatenando strings com dados do usuário, o invasor pode manipular a consulta para obter acesso não autorizado, modificar dados ou até mesmo deletar tabelas inteiras.
Por exemplo, considere uma consulta para autenticação de usuário: SELECT * FROM usuarios WHERE email = '$email' AND senha = '$senha'. Se o invasor digitar no campo de email ' OR '1'='1, a consulta se torna SELECT * FROM usuarios WHERE email = '' OR '1'='1' AND senha = '$senha'. Como '1'='1' é sempre verdadeiro, o invasor pode acessar o sistema sem saber a senha.
Exemplo de código vulnerável:
$email = $_POST['email'];
$senha = $_POST['senha'];
$sql = "SELECT * FROM usuarios WHERE email = '$email' AND senha = '$senha'";
$resultado = $conn->query($sql);Por que prepared statements protegem
Prepared statements (declarações preparadas) funcionam em duas etapas: primeiro, o banco de dados compila o esqueleto da consulta com placeholders (marcadores) no lugar dos valores; depois, os valores são enviados separadamente, garantindo que nunca sejam interpretados como parte da instrução SQL. Isso impede a injeção porque os dados são tratados como valores literais, não como código executável.
Exemplo com PDO:
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE email = :email AND senha = :senha");
$stmt->execute(['email' => $email, 'senha' => $senha]);
$usuario = $stmt->fetch();No exemplo acima, os placeholders :email e :senha são substituídos pelos valores fornecidos no array, mas o banco de dados sabe que esses são dados, não comandos. Mesmo que o usuário digite ' OR '1'='1, ele será tratado como uma string literal, nunca como parte da lógica SQL.
Além disso, prepared statements podem melhorar a performance em consultas repetitivas, pois a consulta é compilada uma única vez e reutilizada com diferentes valores.
Erros comuns
Muitos desenvolvedores tentam prevenir SQL injection de formas inadequadas. Os erros mais comuns incluem:
- Usar funções de escape de forma incorreta: Funções como
mysqli_real_escape_string()podem ajudar, mas não são infalíveis se a codificação do banco de dados não for a mesma. Além disso, é fácil esquecer de escapar algum campo. - Confiar apenas em validação no front-end: A validação JavaScript pode ser contornada facilmente. Toda validação deve ser feita no servidor.
- Construir consultas dinâmicas com string concatenada: Mesmo que você escape os dados, a concatenação ainda permite que um erro de escape resulte em injeção.
- Ignorar prepared statements por preguiça ou desconhecimento: Muitos tutoriais antigos mostram o uso de consultas concatenadas, mas isso não é mais aceitável.
Exemplo de código que parece seguro, mas ainda é vulnerável:
$email = mysqli_real_escape_string($conn, $_POST['email']);
$senha = mysqli_real_escape_string($conn, $_POST['senha']);
$sql = "SELECT * FROM usuarios WHERE email = '$email' AND senha = '$senha'";Se a codificação do banco for diferente da aplicação, ou se o invasor usar caracteres multibyte, a função de escape pode falhar. Portanto, prepared statements são a única forma segura.
Boas práticas
Para garantir a segurança contra SQL injection, siga estas boas práticas:
- Sempre use prepared statements com PDO ou MySQLi. Nunca concatene valores diretamente na consulta.
- Valide e sanitize os dados de entrada conforme as regras de negócio (tipo, formato, tamanho), mas não confie nisso como única defesa.
- Use o modo de exceção do PDO para capturar erros e evitar vazamento de informações.
- Nunca exiba detalhes de erros SQL para o usuário final; registre-os em logs.
- Mantenha o PHP e o banco de dados atualizados para corrigir possíveis vulnerabilidades.
- Considere usar um ORM (Object-Relational Mapping) como Doctrine ou Eloquent, que já utilizam prepared statements internamente.
Exemplo completo de consulta segura com PDO:
try {
$pdo = new PDO('mysql:host=localhost;dbname=teste;charset=utf8', '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();
if ($usuario) {
echo "Login bem-sucedido!";
} else {
echo "Credenciais inválidas.";
}
} catch (PDOException $e) {
// Log do erro
error_log($e->getMessage());
echo "Erro interno. Tente novamente mais tarde.";
}Lembre-se: a segurança é uma responsabilidade contínua. Sempre revise seu código e mantenha-se atualizado sobre as melhores práticas.
Referências
- PHP: PDO - Manual
- OWASP: SQL Injection
- OWASP: SQL Injection Prevention Cheat Sheet
- PortSwigger: SQL Injection
- PHP: SQL Injection - Manual
Exercícios
- Explique com suas palavras o que é SQL injection e como ele pode ser explorado.
- Escreva um trecho de código PHP vulnerável a SQL injection e depois corrija-o usando prepared statements com PDO.
- Por que funções como
mysqli_real_escape_string()não são consideradas seguras como única defesa contra SQL injection? - Crie um exemplo de consulta segura usando PDO que insira um novo usuário na tabela 'usuarios' com campos nome, email e senha.
- Liste três boas práticas para prevenir SQL injection além do uso de prepared statements.
' OR '1'='1 em um campo de email, a consulta se torna sempre verdadeira, contornando a autenticação.Vulnerável:
$id = $_GET['id'];
$sql = "SELECT * FROM produtos WHERE id = $id";
$result = $conn->query($sql);Corrigido com PDO:$pdo = new PDO('mysql:host=localhost;dbname=teste', 'user', 'pass');
$stmt = $pdo->prepare("SELECT * FROM produtos WHERE id = :id");
$stmt->execute(['id' => $_GET['id']]);
$result = $stmt->fetchAll();$pdo = new PDO('mysql:host=localhost;dbname=meubd', 'user', 'pass');
$stmt = $pdo->prepare("INSERT INTO usuarios (nome, email, senha) VALUES (:nome, :email, :senha)");
$stmt->execute([
'nome' => $_POST['nome'],
'email' => $_POST['email'],
'senha' => password_hash($_POST['senha'], PASSWORD_DEFAULT)
]);