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_string pode 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

Exercícios

  1. Explique por que a concatenação de strings em SQL é perigosa, dando um exemplo de ataque.
  2. ✓ Resposta: A concatenação permite que a entrada do usuário seja interpretada como parte do SQL. Por exemplo, se a consulta é 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.
  3. 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.
  4. ✓ Resposta:
  5. Qual é a diferença entre usar PDO::ATTR_EMULATE_PREPARES => false e o valor padrão? Por que é recomendado?
  6. ✓ Resposta: Por padrão, o PDO pode emular prepared statements no lado do PHP, o que pode ter comportamentos diferentes dependendo do driver. Definir 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.
  7. Considere o código vulnerável abaixo. Reescreva-o usando prepared statements com MySQLi.
  8. $mysqli = new mysqli('localhost', 'user', 'pass', 'db');
    $id = $_GET['id'];
    $result = $mysqli->query("SELECT * FROM produtos WHERE id = $id");
    ?>

    ✓ Resposta:
  9. Cite três boas práticas para evitar SQL injection além do uso de prepared statements.
  10. ✓ Resposta: 1) Validar e filtrar rigorosamente os dados de entrada no servidor; 2) Usar o princípio do menor privilégio no banco de dados (criar um usuário com permissões mínimas); 3) Manter o PHP e os drivers de banco atualizados; 4) Evitar construir SQL dinâmico com base em entrada do usuário; se necessário, usar listas brancas.