Uma das maiores vulnerabilidades de sites, a injeção de SQL ([Somente usuários registrados podem vem os links. ]) é também, no caso do PHP, uma das mais fáceis de prevenir. Infelizmente, muitos não tomam as devidas precauções e acabam tendo os seus dados comprometidos.
SQL Injection
Antes de começar, vale a pena ilustrar como funciona um ataque típico de SQL Injection:
Código PHP:
// Temos esta consulta simples aonde os dados $username e $password vem de um formulário preenchido pelo usuário
$query = "SELECT * FROM tabela WHERE username = '$username'";
// Se não houver validação correta um usuário mal-intencionado poderia colocar algum código SQL no lugar do username:
$username = "' OR 1'";
// A consulta ficaria assim:
$query = "SELECT * FROM tabela WHERE username = "'' OR 1";
// Como a expressão 'OR 1' sempre resulta em TRUE, a consulta retornaria os dados de todos os usuários no sistema
Convenhamos, o exemplo acima é bobo, mas serve para mostrar a teoria por trás da técnica de injeção de sql. Se ainda não se convenceu de que é necessário, veja um exemplo mais grave:
Código PHP:
// começamos com a mesma consulta
$query = "SELECT * FROM tabela WHERE username = '$username'";
// desta vez, o código inserido é bem mal-intencionado mesmo...
$username = "'; DELETE FROM tabela WHERE 1 OR username = '";
// a consulta final ficaria assim:
$query = "SELECT * FROM tabela WHERE username = ''; DELETE FROM tabela WHERE 1 OR username = ''";
// ou seja, se executada, a consulta excluiria todos os registros da tabela
Validação sozinha não resolve!
Você pode estar questionando se uma boa validação já não resolveria o problema, já que em ambos exemplos validar para aceitar somente letras funcionaria para bloquear ambas tentavas. Bom a resposta é: SIM e NÃO!
Por mais que a validação ajude, mesmo usando expressões regulares complexas, há meios de burlá-la utilizando outros charsets e técnicas maliciosas. Segurança nunca é demais e não custa se prevenir para proteger os seus dados ou os dos seus clientes.
O que são prepared statements?
Nada mais são do que consultas “pré-prontas”… A diferença é que em lugar das variáveis você coloca um placeholder (marcador de lugar) e na hora da consulta informa a ordem das variáveis a serem substituidas.
É mais fácil de explicar com um exemplo:
Código PHP:
// a interrogação vai no lugar da variável
$query = "SELECT * FROM tabela WHERE username = ?";
// para fazer com vários parametros é a mesma coisa
$query = "SELECT * FROM tabela WHERE username = ? OR username = ?";
Depois, é só informar o que vai no lugar dos respectivos ‘?’ e a consulta estará protegida! Isto funciona porque ao prepararmos a consulta, avisamos ao MySQL (ou outro [Somente usuários registrados podem vem os links. ] que suporte prepared statements) como é a consulta e exatamente aonde vão as variáveis. Repare que nem precisamos mais colocar aspas em volta da variável, pois ele já sabe que é uma variável e a trata de acordo.
No PHP, a extensão [Somente usuários registrados podem vem os links. ]também suporta statements preparados, mas recomendo sempre utilizar o PDO pois ele facilita a migração para outros bancos, além de oferecer uma API concisa entre eles.
Como Funciona com PDO
Código PHP:
// vamos partir do pressuposto que temos um objeto PDO instanciado e devidamente configurado e iremos trabalhar com a mesma consulta dos exemplos anteriores
$query = "SELECT * FROM tabela WHERE username = ?";
// o método PDO::prepare() retorna um objeto da classe PDOStatement ou FALSE se ocorreu algum erro (neste caso use $pdo->errorInfo() para descobrir o que deu errado)
$stmt = $pdo->prepare($query);
// agora que temos o statement preparado, precisamos "bindar" a variável
$username = "fulano";
// utilizamos o método PDOStatement::bindValue() que aceita como parâmetros a posição do ? que a variável irá substituir (a primeira é 1) e a própria variável
$stmt->bindValue(1, $username);
// executamos o statement
$ok = $stmt->execute();
// agora podemos pegar os resultados (partimos do pressuposto que não houve erro)
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
Parece meio chato, ter que escrever tanto código a mais para executar uma simples consulta e se a questão da segurança não for motivo sufiente, que tal este: statements preparados são mais rápidos que consultas normais! Especialmente se a mesma consulta for executada diversas vezes durante um request (mudando ou não as variáveis).
Como o assunto é extenso, incluirei algumas informações adicionais para esclarecer alguns itens, se não precisar de tanto detalhe técnico pode pular ao final!
Informações Complementares
- Há outro método para fazer “bind” das variáveis: o PDO::bindParam(), a síntaxe é exatamente a mesma mas ele recebe a variável por referência, enquanto PDO::bindValue() recebe por valor.
. - Tanto o bindValue() quanto o bindParam() aceitam um terceiro parâmetro opcional que é o tipo da variável (PDO::PARAM_STR, PDO::PARAM_INT, etc)
. - Se não gostou da sintaxe de colocar varias interrogações e depois substituir as variáveis de acordo com a posição da interrogação, também é possível utilizar placeholders nomeados:
Código PHP:
// em vez da interrogação utilizamos uma palavra prefixada de um ":"
$query = "SELECT * FROM tabela WHERE username = :usuario OR username = :administrador";
// na hora de fazer bind fica mais claro qual variável vai aonde
$stmt->bindValue(":usuario", $username);
$stmt->bindValue(":administrador", "admin");
// este método também facilita fazer binds dentro de loops foreach aonde o placeholder é a chave e a variável é o valor
foreach($variaveis as $k => $v){
$stmt->bindValue(":$k", $v);
}
E lembre-se, ignorar estas dicas na hora de criar um sistema é o famoso caso do “barato que sai caro”.