Categorias

Evite SQL Injection usando Prepared Statements no PHP

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:

// 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:

// 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!

// 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

// 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:
    // 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