Nesse tutorial vamos entender o básico do SQL injection, como funciona e como consertar essa vulnerabilidade, depois faremos outro post demonstrando técnicas mais avançadas de injeção de SQL e como atravessar as barreiras de segurança.
Para isso vamos entrar em um portal vulnerável com uma senha errada e ver o que o log nos diz:
Tentaremos entrar com o Login de Alice, alguém que tem login e senha, porém não sabemos a senha dela.
Esse é o resultado do LOG
[1m[35mUser Load (0.3ms)[0m SELECT `users`.* FROM `users` WHERE (email = 'alice@bank.com' AND password_hash = 'alice123') ORDER BY `users`.`id` ASC LIMIT 1
Então, a senha alice123 não parece funcionar para a conta de Alice. Agora vamos tentar inserir a mesma senha seguido do caractere de uma única aspa '.
Inserimos:
Nome de usuário: alice@bank.com
Senha: alice123'
Resultado
Algo quebrou. Adicionar a aspa única à senha causou a falha no aplicativo TradePORTAL com um erro de servidor interno HTTP 500
Olhando para o painel de log, parece ter sido devido ao erro de sintaxe SQL
Vamos ler com cuidado a saída do log de erro. vemos que a aspa única ' na senha de Alice causou esse erro
[1m[36mUser Load (0.6ms)[0m [1mSELECT `users`.* FROM `users` WHERE (email = 'alice@bank.com' AND password_hash = 'alice123'') ORDER BY `users`.`id` ASC LIMIT 1[0m
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''alice123'') ORDER BY `users`.`id` ASC LIMIT 1' at line 1: SELECT `users`.* FROM `users` WHERE (email = 'alice@bank.com' AND password_hash = 'alice123'') ORDER BY `users`.`id` ASC LIMIT 1
Entendendo o erro
Para entender por que o erro ocorreu, vamos primeiro analisar o método de autenticação do TradePORTAL, especificamente o código responsável por verificar as credenciais de autenticação de Alice.
Linha 1. $email=$_POST['email'];
Para validar as credenciais do usuário, o elemento de array _POST [] que contém o endereço de e-mail (passado no request POST) é atribuído à variável local $email
Linha 2. $password=$_POST['password'];
Da mesma forma, o valor da senha enviada da Alice é extraído da Array _POST [] e atribuído à senha local $password
linha 3. $stmt=mysql_query("SELECT * FROM users WHERE (email='$email' AND password='$password') LIMIT 0,1");
A variável string $stmt é então declarada, que representa a consulta SQL usada para autenticar as credenciais de Alice.
Observe que os valores de $email e $password da Alice são concatenados para criar a query final.
Linha 4. $count = mysql_fetch_array($stmt);
A SQL query definida na variável string $stmt é então executada invocando o método mysql_fetch_array. Este método executa nossa consulta ao servidor backend SQL que é verificado na linha 8 através do bloco if/else.
Finalmente, se as credenciais de Alice coincidirem, o método session_start() é chamado e ela é redirecionada para sua página de perfil.
Para entender melhor como a entrada de Alice é usada para construir a instrução SQL usada para autenticação, vamos simplificar o código-fonte de autenticação para o aplicativo TradePORTAL e ver o que acontece.
Agora que simplificamos nosso código de lógica de autenticação, vamos analisar a consulta SQL gerada pelo TradePORTAL para validar as credenciais de autenticação de Alice.
Voltando ao aplicativo vamos seguir os seguintes passos:
Passo 1: Digite a senha alice123' e veja o que acontece ao código.
Observe como a aspa única ' que anexamos é interpretada pelo servidor SQL como um delimitador de string.
No entanto, quando a consulta é processada, a última citação não possui um caractere de fechamento/correspondência, o que causa um erro de sintaxe SQL, resultando no erro de servidor interno HTTP 500
Passo 2: Agora vamos tentar iniciar sessão com uma senha seguida por duas aspas simples. por exemplo. alice123''
Curiosamente, o aplicativo não apresenta um erro neste caso, pois o delimitador de string possui um fechamento
Ultrapassando a autenticação
Neste ponto, sabemos que a injeção de caracteres interpretados pelo servidor de banco de dados é conhecida como SQL Injection
No entanto, não são apenas as aspas únicas ' caracteres que podem ser injetados, Strings inteiras podem ser injetadas. E se isso pudesse ser usado para alterar completamente a finalidade da instrução SQL?
Vamos tentar digitar as seguintes credenciais:
Nome de usuário: alice@bank.com
Senha: ' or 1=1) #
Note que no MySQL o caractere # é usado para comentários de código. Fique atento ao código, tudo à direita do caractere # é um comentário .
Dessa forma passamos pelas credenciais sem problemas, mas porque?
Lembre-se de que as seguintes entradas foram enviadas:
Nome de usuário: alice@bank.com
Senha: 'ou 1 = 1) #
O resultado acima resultou na seguinte instrução SQL:
SELECT *
WHERE (email = 'alice@bank.com'
AND password = ' ' OR 1 = 1) #
Como a declaração é sintaticamente válida e OR 1 = 1 sempre retorna true, o mecanismo de autenticação foi ignorado. Vamos agora analisar a vulnerabilidade a partir de uma perspectiva de código.
Para recapitular rapidamente, os elementos da array _POST[] são extraídos do nome de usuário e senha da Alice), que são atribuídos às variáveis $email e $password locais.
A variável string $stmt é então declarada, que representa a consulta SQL usada para autenticar as credenciais de Alice.
Observe que a entrada de $email e $password de Alice (recuperada usando a array _POST[] são concatenadas dentro da variável $stmt, sem verificações de validação de entrada.
Como não há validação de entrada (ou seja, não há verificação de caracteres permitidos, tamanho maximo ou minimo de strings ou remoção de caracteres "maliciosos", Alice tem a capacidade de injetar sintaxe SQL bruta nos campos de entrada de nome de usuário e senha para alterar o significado do consulta SQL responsável pela autenticação, resultando em um bypass do mecanismo de autenticação do aplicativo ... um ataque de Injeção SQL!
Corrigindo
Prepared statements, Declarações preparadas (também conhecidas como aka parameterized queries) são o melhor mecanismo para prevenir ataques de injeção SQL.
As declarações preparadas são usadas para abstrair a sintaxe da instrução SQL dos parâmetros de entrada. Os modelos de declaração são primeiro definidos na camada do aplicativo, e os parâmetros são passados para eles.
No PHP, isso pode ser efetuado usando um prepare method para enviar instruções SQL no banco de dados do backend.
Além de uma melhor postura de segurança contra ataques de injeção SQL, as declarações preparadas oferecem qualidade de código, desde uma perspectiva de legibilidade e manutenção, devido à separação da lógica SQL de suas entradas(inputs).
Vamos ver o código antes do reparo
E depois
$email=$_POST['email'];
$password=$_POST['password'];
//$stmt=mysql_query("SELECT * FROM users WHERE (email='$email' AND password='$password') LIMIT 0,1");
$stmt = $db->prepare("SELECT * FROM users WHERE (email=:email AND password=:password) LIMIT 0,1");
$stmt->bindParam(':email', $email);
$stmt->bindParam(':password', $password);
$stmt->execute();
$count = $stmt->rowCount();
//$count = mysql_fetch_array($stmt);
if($count > 0)
{
session_start();
# Successfully logged in and redirect to user profile page
}
else
{
# Auth failure - Redirect to Login Page
}
Em nosso exemplo de código modificado, primeiro declaramos a string de consulta de autenticação e passamos isso para prepare method do PHP.
Observe que a variável $e-mail e $password agora foi substituída por :e-mail e :password símbolo que atua como um espaço reservado para o método PHP bind_Param na linha 6 e 7.
O método bindParam() é então chamado para passar o valor do parâmetro $email para a nossa declaração preparada. Esta função leva dois argumentos, o índice de posição do nosso espaço reservado indicado por :email e o valor do parâmetro armazenado na variável $e-mail.
Da mesma forma, o método bindParam() é então chamado para passar o valor do parâmetro $password para a nossa declaração preparada.
Finalmente, executamos nossa consulta de autenticação invocando o método execute(). O SQL usado pelo prepare() é pré compilado garantindo que todos os parâmetros enviados ao banco de dados sejam tratados como valores literais e não instrução SQL/query, garantindo que nenhum código SQL possa ser injetado.