Injeção SQL: Como Proteger seu Banco de Dados PHP (Guia Completo)

Você acredita que seu banco de dados SQL opera de forma eficiente e está imune a danos súbitos? Bem, a Injeção SQL discorda veementemente!

Sim, estamos falando de destruição imediata, pois não desejo iniciar este artigo com a terminologia habitual de “reforçar a segurança” e “evitar acesso malicioso”. Injeção SQL é uma tática tão antiga que todos, cada desenvolvedor, a conhece bem e sabe como preveni-la. Exceto, talvez, por aquele momento raro de deslize, cujas consequências podem ser desastrosas.

Se você já sabe o que é Injeção SQL, sinta-se à vontade para prosseguir para a segunda parte deste artigo. Mas, para aqueles que estão começando no desenvolvimento web e almejam papéis mais importantes, uma introdução se faz necessária.

O que é Injeção SQL?

A chave para compreender a Injeção SQL está em seu próprio nome: SQL + Injeção. O termo “injeção”, nesse contexto, não se refere a procedimentos médicos, mas sim ao ato de “injetar”. Juntas, essas duas palavras transmitem a ideia de inserir SQL em uma aplicação web.

Inserindo SQL em uma aplicação web… hmm… Não é exatamente o que fazemos de qualquer forma? Sim, porém, não queremos que um invasor controle nosso banco de dados. Vamos analisar isso com um exemplo.

Imagine que você está criando um site PHP para uma loja virtual local e decide incluir um formulário de contato como este:

    <form action="record_message.php" method="POST">
      <label>Seu nome</label>
      <input type="text" name="name">
      
      <label>Sua mensagem</label>
      <textarea name="message" rows="5"></textarea>
      
      <input type="submit" value="Enviar">
    </form>

E, vamos supor que o arquivo `send_message.php` armazene tudo em um banco de dados, para que os proprietários da loja possam ler as mensagens dos usuários posteriormente. O código pode ser semelhante a este:

    <?php

    $name = $_POST['name'];
    $message = $_POST['message'];

    // Verifica se este usuário já tem uma mensagem
    mysqli_query($conn, "SELECT * from mensagens where nome = $name");

    // Outro código aqui

Inicialmente, você busca verificar se este usuário já possui uma mensagem não lida. A consulta `SELECT * from mensagens where nome = $nome` parece simples, certo?

ERRADO!

Em nossa inocência, abrimos as portas para a destruição imediata do nosso banco de dados. Para que isso aconteça, o invasor deve ter as seguintes condições satisfeitas:

  • O aplicativo está operando em um banco de dados SQL (hoje, quase todos os aplicativos estão).
  • A conexão atual com o banco de dados possui permissões de “editar” e “excluir” no banco de dados.
  • Os nomes das tabelas relevantes podem ser descobertos.

O terceiro ponto significa que, uma vez que o invasor sabe que você administra uma loja virtual, é muito provável que os dados dos pedidos sejam armazenados em uma tabela de pedidos. Com tudo isso em mãos, tudo o que o invasor precisa fazer é fornecer o seguinte como seu nome:

Joe; truncate orders;? Sim senhor! Vamos observar como a consulta se torna quando executada pelo script PHP:

SELECT * FROM mensagens WHERE nome = Joe; truncate orders;

Ok, a primeira parte da consulta tem um erro de sintaxe (ausência de aspas em torno de “Joe”), mas o ponto e vírgula força o motor MySQL a iniciar a interpretação de uma nova consulta: `truncate orders`. Assim, de uma vez, todo o histórico de pedidos é apagado!

Agora que você sabe como a Injeção SQL funciona, é hora de aprender a detê-la. As duas condições necessárias para uma Injeção SQL bem-sucedida são:

  • O script PHP deve ter privilégios de modificação/exclusão no banco de dados. Isso se aplica a praticamente todos os aplicativos, e você não poderá torná-los somente leitura. Mesmo que removamos todas as permissões de modificação, a Injeção SQL ainda pode permitir que alguém execute consultas SELECT e visualize todo o banco de dados, incluindo informações confidenciais. Em outras palavras, diminuir o nível de acesso ao banco de dados não resolve, e seu aplicativo precisa desse acesso.
  • A entrada do usuário está sendo processada. A Injeção SQL só funciona quando você aceita dados de usuários. Novamente, não é viável interromper todas as entradas do seu aplicativo apenas por preocupação com a Injeção SQL.

Como Evitar Injeção SQL em PHP

Dado que conexões com banco de dados, consultas e entradas de usuários são inevitáveis, como evitamos a Injeção SQL? Felizmente, é simples: 1) higienizar as entradas do usuário e 2) usar instruções preparadas.

Higienize as Entradas do Usuário

Se você estiver usando uma versão antiga do PHP (5.5 ou anterior, o que é comum em hospedagens compartilhadas), é aconselhável passar todas as entradas de usuário por uma função chamada `mysql_real_escape_string()`. Essencialmente, essa função remove todos os caracteres especiais de uma string, de forma que eles percam o significado quando usados pelo banco de dados.

Por exemplo, se você tiver uma string como `I’m a string`, a aspa simples (`’`) poderia ser usada por um invasor para manipular a consulta do banco de dados e causar uma Injeção SQL. Passar isso pela função `mysql_real_escape_string()` resultaria em `I\’m a string`, adicionando uma barra invertida à aspa simples, escapando-a. Como resultado, a string toda agora é passada para o banco de dados como inofensiva, em vez de participar da manipulação da consulta.

Há uma desvantagem nessa abordagem: é uma técnica muito antiga que acompanha as formas mais antigas de acesso a banco de dados em PHP. A partir do PHP 7, essa função nem sequer existe mais, o que nos leva à próxima solução.

Usar Declarações Preparadas

Declarações preparadas são uma forma mais segura e confiável de fazer consultas ao banco de dados. A ideia é que, em vez de enviar a consulta diretamente para o banco de dados, nós primeiro informamos a ele a estrutura da consulta. Isso é o que significa “preparar” uma declaração. Depois que uma declaração é preparada, passamos as informações como entradas parametrizadas, para que o banco de dados possa “preencher as lacunas”, ligando as entradas à estrutura da consulta que enviamos. Isso elimina qualquer poder especial que as entradas possam ter, tratando-as como meras variáveis em todo o processo. Observe como as declarações preparadas se parecem:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";

    // Cria conexão
    $conn = new mysqli($servername, $username, $password, $dbname);

    // Verifica conexão
    if ($conn->connect_error) {
        die("Falha na conexão: " . $conn->connect_error);
    }

    // prepara e associa
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);

    // Define parâmetros e executa
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();

    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();

    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();

    echo "Novos registros criados com sucesso";

    $stmt->close();
    $conn->close();
    ?>

Eu sei que o processo pode parecer desnecessariamente complexo se você é novo em declarações preparadas, mas o conceito vale o esforço. Aqui está uma boa introdução.

Para aqueles que já estão familiarizados com a extensão PDO do PHP e a usam para criar declarações preparadas, tenho um pequeno conselho.

Aviso: Cuidado ao Configurar o PDO

Ao usar PDO para acesso ao banco de dados, podemos cair em uma falsa sensação de segurança. “Ah, estou usando PDO. Não preciso me preocupar com mais nada” — é assim que geralmente pensamos. É verdade que PDO (ou declarações preparadas pelo MySQLi) são suficientes para prevenir ataques de Injeção SQL, mas devemos ter cuidado ao configurá-lo. É comum apenas copiar e colar código de tutoriais ou de projetos anteriores e seguir em frente, mas essa configuração pode anular tudo:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

Essa configuração instrui o PDO a emular declarações preparadas em vez de usar o recurso real do banco de dados. Consequentemente, o PHP envia strings de consulta simples ao banco de dados, mesmo que seu código pareça estar criando declarações preparadas e definindo parâmetros. Ou seja, você fica tão vulnerável à Injeção SQL como antes.

A solução é simples: certifique-se de que essa emulação esteja definida como falsa.

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Agora, o script PHP é forçado a usar declarações preparadas em nível de banco de dados, prevenindo ataques de Injeção SQL.

Prevenção com WAF

Você sabia que também é possível proteger aplicativos web contra Injeção SQL utilizando um WAF (Web Application Firewall)?

Além de Injeção SQL, um WAF pode proteger contra diversas vulnerabilidades da camada 7, como cross-site scripting, autenticação comprometida, falsificação entre sites, exposição de dados etc.

Injeção SQL e Frameworks PHP Modernos

Injeção SQL é tão comum, tão fácil, tão frustrante e tão perigosa que todos os frameworks web PHP modernos vêm com contramedidas embutidas. No WordPress, por exemplo, temos a função `$wpdb->prepare()`, e se você estiver usando um framework MVC, ele já faz o trabalho para você, evitando a necessidade de se preocupar com Injeção SQL. É um pouco incômodo que no WordPress você precise preparar declarações explicitamente, mas, bem, é o WordPress de que estamos falando. 🙂

De qualquer forma, o que quero dizer é que a nova geração de desenvolvedores web não se preocupa com Injeção SQL e, consequentemente, desconhece a possibilidade. Portanto, mesmo que eles deixem uma brecha no aplicativo (como um parâmetro de consulta `$_GET` com hábitos antigos de criar uma consulta impura), os resultados podem ser catastróficos. Por isso, é sempre bom reservar um tempo para se aprofundar nos fundamentos.

Conclusão

Injeção SQL é um ataque nocivo em aplicativos web, mas é facilmente evitável. Como vimos neste artigo, é necessário ter cuidado ao processar as entradas de usuário (Injeção SQL não é a única ameaça que o tratamento de entradas de usuário traz) e ao consultar o banco de dados. Dito isso, nem sempre estamos trabalhando com a segurança de um framework web, portanto, é melhor permanecer atento a esse tipo de ataque e não cair nessa armadilha.