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

Exercícios

  1. Explique com suas palavras o que é SQL injection e como ele pode ser explorado.
  2. ✓ Resposta: SQL injection é uma técnica de ataque onde um invasor insere comandos SQL maliciosos através de campos de entrada, como formulários ou URLs, que são executados pelo banco de dados. Isso pode ocorrer quando a aplicação concatena dados do usuário diretamente na consulta SQL, permitindo que o invasor manipule a consulta para obter acesso não autorizado, modificar ou deletar dados. Por exemplo, digitando ' OR '1'='1 em um campo de email, a consulta se torna sempre verdadeira, contornando a autenticação.
  3. Escreva um trecho de código PHP vulnerável a SQL injection e depois corrija-o usando prepared statements com PDO.
  4. ✓ Resposta:
    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();
  5. Por que funções como mysqli_real_escape_string() não são consideradas seguras como única defesa contra SQL injection?
  6. ✓ Resposta: Funções de escape podem falhar quando a codificação do banco de dados não corresponde à da aplicação, permitindo ataques de caracteres multibyte. Além disso, é fácil esquecer de escapar algum campo, e a concatenação ainda permite que um invasor explore a lógica da consulta. Prepared statements eliminam esses riscos ao separar dados da estrutura da consulta, tratando os valores como literais.
  7. Crie um exemplo de consulta segura usando PDO que insira um novo usuário na tabela 'usuarios' com campos nome, email e senha.
  8. ✓ Resposta:
    $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)
    ]);
  9. Liste três boas práticas para prevenir SQL injection além do uso de prepared statements.
  10. ✓ Resposta: 1. Validar e sanitizar dados de entrada conforme as regras de negócio (tipo, formato, tamanho). 2. Usar um ORM que já implementa prepared statements internamente. 3. Manter o PHP e o banco de dados atualizados para corrigir vulnerabilidades conhecidas.