Introdução: Mitos e realidade do PDO
\n\nSe você programa em PHP e já pesquisou no Google "como se proteger contra injeções SQL", certamente já ouviu o mantra: "Use PDO e prepared statements — e você estará seguro". Este é o padrão ouro, a base de qualquer tutorial sobre segurança em PHP. Mas será que é realmente assim? Basta escrever $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?') e esquecer o problema?
A pergunta "Are PDO prepared statements sufficient to prevent SQL injection?" acumulou milhares de visualizações no Stack Overflow por um motivo. A resposta, como se vê, não é tão simples. Neste artigo, vamos mergulhar nos bastidores do PDO, entender como as consultas preparadas realmente funcionam e descobrir em quais casos até o PDO pode falhar.
\n\nNão é teoria chata. Vamos ver exemplos reais, emulação de consultas, nuances com codificações e dar action items concretos para que seu código esteja verdadeiramente protegido.
\n\nComo funcionam os Prepared Statements: Mágica ou matemática?
\n\nPrimeiro, vamos lembrar o que é uma injeção SQL. É quando um invasor envia dados em uma consulta que são interpretados como parte do código SQL. O clássico: ' OR 1=1 --.
Os prepared statements (consultas preparadas) resolvem esse problema de forma radical. Eles separam a lógica da consulta dos dados. Funciona assim:
\n\n<?php\n$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');\n$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');\n$stmt->execute([':email' => $_GET['email'], ':status' => 'active']);\n?>\n\n\nPor baixo dos panos, acontece o seguinte:
\n\n- \n
- Preparação (Prepare): O driver envia ao servidor do banco de dados o "esqueleto" da consulta:
SELECT * FROM users WHERE email = ? AND status = ?. O servidor analisa esse esqueleto, otimiza-o e armazena o plano de execução. O lugar para os dados (placeholders) fica vazio por enquanto. \n - Execução (Execute): Agora você envia apenas os valores reais. O servidor os insere no plano já pronto, mas de uma forma especial — ele sabe que são dados, não código. Portanto, qualquer tentativa de "fechar aspas" ou adicionar um comando SQL simplesmente se torna parte de um literal de string. \n
Parece ideal. Mas há um detalhe.
\n\nA armadilha: Emulação de Prepared Statements
\n\nA principal surpresa revelada por aquela pergunta no Stack Overflow é: o PDO, por padrão, nem sempre usa prepared statements "reais".
\n\nIsso porque nem todos os bancos de dados suportam esse recurso no nível do protocolo. E alguns drivers (por exemplo, para SQLite ou versões antigas do MySQL) funcionam através de emulação. O que isso significa?
\n\nQuando a emulação está ativada, o próprio PDO substitui os valores na consulta SQL do lado do cliente (em PHP) e, em seguida, envia a consulta já pronta para o servidor. Nesse processo, ele escapa os caracteres especiais usando PDO::quote().
E é aqui que começam os problemas. A emulação é, essencialmente, a mesma substituição de strings, só que feita pela biblioteca. E se você definir a codificação da conexão incorretamente, esse mecanismo pode ser quebrado.
\n\nExemplo: Ataque através de codificações diferentes
\n\nO ataque clássico usando codificação incorreta funciona assim. Suponha que você configurou a conexão com MySQL na codificação GBK (codificação chinesa), e a consulta é emulada. O caractere \\ (barra invertida) em GBK pode fazer parte de um caractere multibyte.
O invasor envia a string: %BF%27. Na codificação GBK, a sequência %BF%5C forma um único caractere, e %27 é apenas uma aspa. O emulador vê a aspa e a escapa, transformando %27 em %5C%27 (ou seja, \\'). Mas, devido ao fa