O que é SQL Injection e como prevenir em aplicações PHP?

Então você acha que seu banco de dados SQL tem bom desempenho e está protegido contra destruição instantânea? Bem, SQL Injection discorda!

Sim, estamos falando de destruição instantânea, porque não quero abrir este artigo com a terminologia esfarrapada usual de “reforçar a segurança” e “impedir o acesso malicioso”. SQL Injection é um truque tão antigo no livro que todo mundo, todo desenvolvedor, conhece muito bem e sabe como evitá-lo. Exceto por aquele momento estranho em que eles escorregam, e os resultados podem ser nada menos que desastrosos.

Se você já sabe o que é SQL Injection, fique à vontade para pular para a segunda metade do artigo. Mas para aqueles que estão começando no campo do desenvolvimento web e sonham em assumir funções mais importantes, alguma introdução é necessária.

O que é SQL Injection?

A chave para entender SQL Injection está em seu nome: SQL + Injection. A palavra “injeção” aqui não tem nenhuma conotação médica, mas sim o uso do verbo “injetar”. Juntas, essas duas palavras transmitem a ideia de colocar o SQL em um aplicativo da web.

Colocando SQL em um aplicativo da web. . . hmmm . . . Não é isso que estamos fazendo de qualquer maneira? Sim, mas não queremos que um invasor controle nosso banco de dados. Vamos entender isso com a ajuda de um exemplo.

Digamos que você esteja construindo um site PHP típico para uma loja de comércio eletrônico local, então decide adicionar um formulário de contato como este:

<form action="record_message.php" method="POST">
  <label>Your name</label>
  <input type="text" name="name">
  
  <label>Your message</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Send">
</form>

E vamos supor que o arquivo send_message.php armazene tudo em um banco de dados para que os donos da loja possam ler as mensagens do usuário posteriormente. Pode ter algum código como este:

<?php

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

// check if this user already has a message
mysqli_query($conn, "SELECT * from messages where name = $name");

// Other code here

Então você está primeiro tentando ver se este usuário já tem uma mensagem não lida. A consulta SELECT * de mensagens onde nome = $nome parece bastante simples, certo?

ERRADO!

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

  • O aplicativo está sendo executado em um banco de dados SQL (hoje, quase todos os aplicativos são)
  • A conexão de banco de dados atual tem permissões de “editar” e “excluir” no banco de dados
  • Os nomes das tabelas importantes podem ser adivinhados
  Corrigir o Bootstrapper de instalação da Microsoft parou de funcionar

O terceiro ponto significa que, agora que o invasor sabe que você está executando uma loja de comércio eletrônico, é muito provável que você esteja armazenando os dados do pedido em uma tabela de pedidos. Armado com tudo isso, tudo o que o invasor precisa fazer é fornecer isso como seu nome:

Joe; ordens truncadas;? Sim senhor! Vamos ver como a consulta se tornará quando for executada pelo script PHP:

SELECT * FROM mensagens WHERE nome = Joe; ordens truncadas;

Ok, a primeira parte da consulta tem um erro de sintaxe (sem aspas em torno de “Joe”), mas o ponto e vírgula força o mecanismo do MySQL a começar a interpretar um novo: truncar ordens. Assim, de uma só vez, todo o histórico de pedidos desaparece!

Agora que você sabe como o SQL Injection funciona, é hora de ver como pará-lo. As duas condições que precisam ser atendidas para uma injeção de SQL bem-sucedida são:

  • O script PHP deve ter privilégios de modificação/exclusão no banco de dados. Acho que isso é verdade para todos os aplicativos e você não poderá torná-los somente leitura. 🙂 E adivinhe, mesmo se removermos todos os privilégios de modificação, a injeção de SQL ainda pode permitir que alguém execute consultas SELECT e visualize todo o banco de dados, incluindo dados confidenciais. Em outras palavras, reduzir o nível de acesso ao banco de dados não funciona e seu aplicativo precisa disso de qualquer maneira.
  • A entrada do usuário está sendo processada. A única maneira de a injeção de SQL funcionar é quando você aceita dados de usuários. Mais uma vez, não é prático interromper todas as entradas de seu aplicativo apenas porque você está preocupado com a injeção de SQL.
  • Evitando a injeção de SQL no PHP

    Agora, dado que conexões de banco de dados, consultas e entradas do usuário fazem parte da vida, como evitamos a injeção de SQL? Felizmente, é bem simples e há duas maneiras de fazer isso: 1) higienizar a entrada do usuário e 2) usar instruções preparadas.

    Higienize a entrada do usuário

    Se você estiver usando uma versão mais antiga do PHP (5.5 ou inferior, e isso acontece muito em hospedagem compartilhada), é aconselhável executar todas as entradas do usuário por meio de uma função chamada mysql_real_escape_string(). Basicamente, o que ele faz é remover todos os caracteres especiais de uma string para que eles percam o significado quando usados ​​pelo banco de dados.

    Por exemplo, se você tiver uma string como I’m a string, o caractere de aspas simples (‘) pode ser usado por um invasor para manipular a consulta do banco de dados que está sendo criada e causar uma injeção de SQL. Executá-lo através de mysql_real_escape_string() produz que sou uma string, que adiciona uma barra invertida às aspas simples, escapando dela. Como resultado, toda a string agora é passada como uma string inofensiva para o banco de dados, em vez de poder participar da manipulação da consulta.

      Como ver por que a Apple está cobrando seu cartão de crédito

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

    Usar declarações preparadas

    As instruções preparadas são uma maneira de fazer consultas ao banco de dados com mais segurança e confiabilidade. A ideia é que, em vez de enviar a consulta bruta ao banco de dados, primeiro informamos ao banco de dados a estrutura da consulta que enviaremos. Isso é o que queremos dizer com “preparar” uma declaração. Uma vez preparada uma declaração, passamos as informações como entradas parametrizadas para que o banco de dados possa “preencher as lacunas” conectando as entradas à estrutura de consulta que enviamos anteriormente. Isso tira qualquer poder especial que as entradas possam ter, fazendo com que sejam tratadas como meras variáveis ​​(ou cargas úteis, se preferir) em todo o processo. Veja como são as declarações preparadas:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Create connection
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Check connection
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // prepare and bind
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // set parameters and execute
    $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 "New records created successfully";
    
    $stmt->close();
    $conn->close();
    ?>

    Eu sei que o processo parece desnecessariamente complexo se você for novo em declarações preparadas, mas o conceito vale o esforço. aqui está uma boa introdução a ele.

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

    Aviso: Tenha cuidado ao configurar o PDO

    Ao usar o PDO para acesso ao banco de dados, podemos ser sugados por uma falsa sensação de segurança. “Ah, bem, estou usando DOP. Agora não preciso pensar em mais nada” — é assim que nosso pensamento geralmente funciona. É verdade que o PDO (ou declarações preparadas pelo MySQLi) é suficiente para evitar todos os tipos de ataques de injeção de SQL, mas você deve ter cuidado ao configurá-lo. É comum apenas copiar e colar o código de tutoriais ou de seus projetos anteriores e seguir em frente, mas essa configuração pode desfazer tudo:

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

    O que essa configuração faz é dizer ao PDO para emular instruções preparadas em vez de realmente usar o recurso de instruções preparadas do banco de dados. Conseqüentemente, o PHP envia strings de consulta simples para o banco de dados, mesmo que seu código pareça estar criando declarações preparadas e definindo parâmetros e tudo isso. Em outras palavras, você está tão vulnerável à injeção de SQL quanto antes. 🙂

      Corrigir o erro Kodi não é possível criar o aplicativo saindo

    A solução é simples: verifique se essa emulação está definida como falsa.

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

    Agora o script PHP é forçado a usar instruções preparadas em um nível de banco de dados, impedindo todos os tipos de injeção de SQL.

    Prevenção usando WAF

    Você sabia que também pode proteger aplicativos da Web contra injeção de SQL usando WAF (firewall de aplicativo da Web)?

    Bem, não apenas a injeção de SQL, mas muitas outras vulnerabilidades da camada 7, como script entre sites, autenticação quebrada, falsificação entre sites, exposição de dados etc.

    Injeção de SQL e estruturas PHP modernas

    A injeção de 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 integradas. No WordPress, por exemplo, temos a função $wpdb->prepare(), ao passo que se você estiver usando um framework MVC, ele faz todo o trabalho sujo para você e você nem precisa pensar em impedir a injeção de SQL. É um pouco chato que no WordPress você tenha que preparar declarações explicitamente, mas ei, é do WordPress que estamos falando. 🙂

    De qualquer forma, o que quero dizer é que a geração moderna de desenvolvedores da Web não precisa pensar em injeção de SQL e, como resultado, eles nem estão cientes da possibilidade. Dessa forma, mesmo que eles deixem um backdoor aberto em seu aplicativo (talvez seja um parâmetro de consulta $_GET e os velhos hábitos de disparar uma consulta suja), os resultados podem ser catastróficos. Portanto, é sempre melhor reservar um tempo para mergulhar mais fundo nas fundações.

    Conclusão

    SQL Injection é um ataque muito desagradável em um aplicativo da web, mas é facilmente evitado. Como vimos neste artigo, ter cuidado ao processar a entrada do usuário (aliás, SQL Injection não é a única ameaça que o manuseio da entrada do usuário traz) e consultar o banco de dados é tudo o que há para fazer. Dito isso, nem sempre estamos trabalhando na segurança de um framework web, então é melhor ficar atento a esse tipo de ataque e não cair nessa.