Prevenindo SQL injection
Aula completa sobre prevenção de SQL injection em PHP, cobrindo o conceito, a proteção oferecida por prepared statements, erros comuns e boas práticas, com exemplos práticos, exercícios e referências.
A injeção de SQL (SQL injection) é uma das vulnerabilidades mais críticas e comuns em aplicações web. Ela ocorre quando um atacante consegue inserir comandos SQL arbitrários em uma consulta, manipulando os dados da aplicação, roubando informações ou até mesmo comprometendo o servidor. Nesta aula, vamos explorar o que é SQL injection, como os prepared statements protegem sua aplicação, quais erros comuns devemos evitar e quais boas práticas adotar para escrever código PHP seguro.
Vamos abordar desde os conceitos básicos até exemplos práticos de código, mostrando como implementar consultas seguras usando PDO e MySQLi. Ao final, você terá um entendimento sólido de como proteger suas aplicações contra essa ameaça.
O que é
SQL injection é uma técnica de ataque que explora falhas na validação de entrada de dados. Quando uma aplicação constrói consultas SQL concatenando strings diretamente com a entrada do usuário, um atacante pode manipular a consulta para executar comandos maliciosos. Por exemplo, em um formulário de login, o campo de usuário pode receber algo como ' OR '1'='1, fazendo com que a consulta retorne sempre verdadeiro e permitindo acesso sem credenciais válidas.
Os impactos de uma injeção de SQL podem ser devastadores: exposição de dados sensíveis, perda de integridade dos dados, exclusão de tabelas, e até execução de comandos no sistema operacional em cenários extremos. Portanto, é fundamental entender como prevenir essa vulnerabilidade.
Exemplo de código vulnerável:
Se o usuário digitar admin' -- no campo de usuário, a consulta se torna SELECT * FROM users WHERE username = 'admin' --' AND password = ..., ignorando a verificação de senha.
Por que prepared statements protegem
Prepared statements (declarações preparadas) separam a estrutura da consulta SQL dos dados fornecidos pelo usuário. Em vez de concatenar strings, você define a consulta com placeholders (? ou :nome) e depois envia os parâmetros separadamente. O banco de dados então trata esses parâmetros como dados puros, nunca como parte do SQL. Isso neutraliza a injeção porque o conteúdo do parâmetro não é interpretado como SQL.
Em PHP, você pode usar PDO ou MySQLi para prepared statements. Com PDO, por exemplo, a consulta é preparada e executada com os valores vinculados. O driver envia a consulta e os parâmetros de forma separada ao banco, garantindo que nada além dos dados seja interpretado.
Exemplo com PDO (seguro):
Além de proteger contra injeção, prepared statements também podem melhorar o desempenho em consultas repetidas, pois o banco pode reutilizar o plano de execução.
Erros comuns
Apesar de ser uma prática conhecida, muitos desenvolvedores ainda cometem erros que deixam suas aplicações vulneráveis. Alguns erros comuns incluem:
- Concatenar strings em consultas SQL: Mesmo com sanitização, a concatenação é arriscada. A melhor abordagem é sempre usar prepared statements.
- Usar funções de escape inadequadas:
mysqli_real_escape_stringpode ajudar, mas não é à prova de falhas em todos os cenários (ex.: charset). Prefira prepared statements. - Confiar em validação no cliente: A validação via JavaScript pode ser contornada. A segurança deve ser sempre no servidor.
- Ignorar erros de exceção: Se o PDO estiver no modo de erro silencioso, falhas podem passar despercebidas. Configure o modo de erro para exceções.
- Usar consultas dinâmicas sem prepared statements: Em casos de ORDER BY ou nomes de tabelas, a construção dinâmica é necessária, mas deve ser feita com cuidado, usando listas brancas.
Exemplo de erro comum: usar addslashes() ou mysql_real_escape_string() (função obsoleta) em vez de prepared statements.
// Errado: concatenação com escape
$username = mysqli_real_escape_string($conn, $_POST['username']);
$sql = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $sql);
// Ainda vulnerável em alguns casos, e desnecessário se usar prepared statements
?>Boas práticas
Para garantir a segurança de suas aplicações, siga estas boas práticas:
- Sempre use prepared statements para qualquer consulta que envolva dados do usuário, seja com PDO ou MySQLi.
- Configure o PDO para lançar exceções em caso de erro, usando
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION. - Valide e filtre os dados no servidor, mesmo usando prepared statements. Por exemplo, verifique se um e-mail tem formato válido.
- Escape a saída para prevenir XSS, mas não confie nisso para SQL.
- Use o princípio do menor privilégio no banco de dados: o usuário da aplicação não deve ter permissões administrativas.
- Evite construir SQL dinâmico com base em entrada do usuário. Se precisar, use uma lista branca de valores permitidos.
- Mantenha o PHP e as bibliotecas atualizados para corrigir vulnerabilidades conhecidas.
Exemplo de configuração segura com PDO:
Além disso, considere usar um ORM (Object-Relational Mapping) como Doctrine ou Eloquent, que já implementa prepared statements internamente, reduzindo o risco de erros manuais.
Boas práticas adicionais
Quando você precisar construir consultas com partes dinâmicas (como ORDER BY), nunca concatene diretamente a entrada do usuário. Em vez disso, mapeie os valores permitidos:
$allowedColumns = ['name', 'email', 'created_at'];
$orderBy = $_GET['order'] ?? 'name';
if (!in_array($orderBy, $allowedColumns)) {
$orderBy = 'name'; // fallback seguro
}
$stmt = $pdo->prepare("SELECT * FROM users ORDER BY $orderBy");
$stmt->execute();
?>Nesse caso, o nome da coluna é controlado por uma lista branca, então não há risco de injeção.
Referências
- PHP Manual: Prepared Statements
- OWASP: SQL Injection
- MySQL Documentation: SQL Injection
- PHP Manual: MySQLi Prepared Statements
- PortSwigger: SQL Injection
- W3Schools: SQL Injection
Exercícios
- Explique por que a concatenação de strings em SQL é perigosa, dando um exemplo de ataque.
- Escreva um código PHP usando PDO com prepared statements para inserir um novo usuário em uma tabela
users(campos: nome, email, senha). Use placeholders nomeados. - Qual é a diferença entre usar
PDO::ATTR_EMULATE_PREPARES => falsee o valor padrão? Por que é recomendado? - Considere o código vulnerável abaixo. Reescreva-o usando prepared statements com MySQLi.
- Cite três boas práticas para evitar SQL injection além do uso de prepared statements.
SELECT * FROM users WHERE id = '$id' e o usuário passa 1 OR 1=1, a consulta retorna todos os registros, pois a condição se torna sempre verdadeira. Isso pode expor dados ou permitir ações não autorizadas.PDO::ATTR_EMULATE_PREPARES => false faz com que o banco de dados use prepared statements nativos, o que é mais seguro e pode melhorar o desempenho, pois o banco realmente prepara a consulta. É recomendado para evitar ambiguidades e garantir a proteção real.$mysqli = new mysqli('localhost', 'user', 'pass', 'db');
$id = $_GET['id'];
$result = $mysqli->query("SELECT * FROM produtos WHERE id = $id");
?>