Próximo post

Criando campos condicionais no Drupal 7

Leia mais»
jun 23 2010

Evite SQL Injection usando Prepared Statements

Uma das maiores vulnerabilidades de sites, a injeção de SQL (SQL Injection) é 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.

Neste tutorial, irei demonstrar como trabalhar com prepared statements usando a extensão PDO do PHP.

SQL Injection

Antes de começar, vale a pena ilustrar como funciona um ataque típico de SQL Injection:

1
2
3
4
56
7
8
9
10
// 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:

1
2
3
4
56
7
8
9
10
// 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!

1
2
3
4
5
// 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 RDBMS 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 MySQLi 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
1
2
3
4
56
7
8
9
1011
12
13
14
1516
17
18
19
20
// 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:
1
2
3
4
56
7
8
9
1011
// 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 é isto, chegamos ao final! Espero que o artigo tenha ajudado e lembre-se, pular estes simples passos é o típico barato que sai caro!

[]s

Se curtiu este post, por favor tome alguns segundos para compartilhá-lo usando os links do lado esquerdo! Valeu!!!