Segurança em Sistemas de Login: Proteção Contra SQL Injection

Introdução

Neste segundo artigo sobre segurança em sistemas de login, abordarei formas de proteção contra SQL Injection.

Existem muitas discussões na Internet, em listas de discussão e fóruns, sobre qual seria a função perfeita para impedir ataque por SQL Injection SQL Injection. Alguns programadores até criam funções que removem, por segurança, palavras-chave da linguagem SQL, como SELECT, DROP, DELETE. Isso pode até resolver, mas não podemos danificar a informação; se permitirmos que o usuário escreva informações em nosso site, devemos permitir-lhe escrever
SELECT, DROP e DELETE também. Imagine, por exemplo, um fórum sobre programação: como poderíamos postar códigos SQL se o fórum removesse as palavras SELECT, DELETE etc? Logo, não podemos remover essas palavras.

A solução é muito simples! Sim, é simples, mesmo. Muitos querem complicar à toa, porém é muito simples: escapar caracteres especiais.

Esses caracteres especiais podem variar conforme o SGBD que se está utilizando. Normalmente são aspas simples e duplas, as quais delimitam strings em um comando SQL.

Vamos a um exemplo. Considere a SQL abaixo:

$sql = "SELECT id, nome, sobrenome FROM autores WHERE nome = '" . $nome . "'
		AND sobrenome = '" . $sobrenome . "'";

Supondo que $nome contenha jo’sé, e $sobrenome, silva, a SQL ficará assim:

SELECT id, nome, sobrenome FROM autores WHERE nome = 'jo'se' AND sobrenome = 'silva';

Isso gera um erro de sintaxe, sem comprometer o banco de dados. Porém, se mantivermos $sobrenome e definirmos $nome com o valor jo’; DROP TABLE autores ; —, teremos:

SELECT id, nome, sobrenome FROM autores WHERE nome = 'jo'; DROP TABLE autores ;
--'AND sobrenome = 'silva';

Dessa forma, selecionam-se os registros com nome igual a “jo”, remove-se a tabela “autores” e considera-se ‘ AND sobrenome = ‘silva’; como comentário. Isso caracteriza um ataque por SQL Injection.

Magic Quotes

Face aos possíveis grandes danos que SQL Injection pode causar, o PHP possui um mecanismo nativo automático para escapar caracteres especiais: o magic quotes. Porém, esse é um mecanismo genérico, que não pode ser aplicado a todos os SGBDs. Logo, não o utilize!

O próprio Manual do PHP não recomenda seu uso:

Não existe mais razão para usar magic quotes porque não é mais uma parte suportada do PHP. Entretanto, a função existe e ajuda alguns iniciantes a construir um código melhor(mais seguro). Mas, ao lidar com código que utiliza este recurso é melhor atualizar o código do que ativar magic quotes. Assim, porque isso existe? Simples, para ajuda a prevenir injeção de SQL. Os desenvolvedores de hoje estão mais a par de segurança e acabam usando mecanismos específicos do banco de dados para escapar e/ou comandos preparados ao invés de depender de coisas como magical quotes.

Fonte: http://php.net/manual/pt_BR/security.magicquotes.why.php

Como citado no trecho, é preferível adaptar seus códigos a fim de torná-los seguros e não vulneráveis a SQL Injection a habilitar o magic quotes. Portanto mantenha a diretiva magic_quotes_gpc, do , em off! Dê preferência a funções específicas para cada SGBD.

Leia o capítulo sobre Magic Quotes, do Manual do PHP, no link abaixo:
http://php.net/manual/pt_BR/security.magicquotes.php

Soluções Especificas para cada SGBD

Vou mostrar algumas soluções específicas para cada SGBD. Mais adiante, falarei sobre uma solução genérica, aplicável a qualquer SGBD, utilizando Prepared Statements.

3.1. MySQL

Vamos ao exemplo mais comum: MySQL: existe uma função específica do PHP para escapar caracteres especiais do MySQL: mysql_real_escape_string.

Ela deve ser usada com magic_quotes_gpc em off. Caso seu servidor mantenha essa diretiva ativa, desabilite-a por meio de htaccess ou, caso isso não seja possível, certifique-se de usar stripslashes antes de aplicar essa função. Veja o exemplo abaixo:

if ( get_magic_quotes_gpc() )
{
    $name = stripslashes( $name );
}
$name = mysql_real_escape_string( $name );
mysql_query( "SELECT * FROM users WHERE name=$name" );

NOTA IMPORTANTE: Vale lembrar que a extensão MySQL está obsoleta desde o PHP 5.5. Ou seja, funções mysql_*, como mysql_connect, mysql_query e semelhantes não devem mais ser usadas. É preferível usar a extensão MySQLi, ou a classe PDO.

3.2. PostgreSQL

O escape de caracteres no PostgreSQL não é feito com barra invertida; é feito com aspas simples. Ou seja, addslashes não funcionaria aqui.

O PHP também tem uma função específica para escape de caracteres especiais para PostgreSQL: pg_escape_string.

Mais informações sobre prevenção de SQL Injection em PostgreSQL podem ser vistas no link abaixo, do Wiki do PostgreSQL:
http://wiki.postgresql.org/wiki/Sql_injection

3.3. Exemplos de códigos para MySQL e PostgreSQL

mysql_connect( 'localhost', 'usuario', 'senha' );
$str = "There's no place like 127.0.0.1, the \"localhost\"";
echo "String:	" . $str . "
";
echo "MySQL:	" . mysql_real_escape_string( $str ) . "
";
echo "Postgre:	" . pg_escape_string( $str ) . "
";

* Para usar mysql\_real\_escape\_string, é necessário uma conexão MySQL ativa.

Saída:

String:	There's no place like 127.0.0.1, the "localhost"
MySQL:	There\'s no place like 127.0.0.1, the \"localhost\"
Postgre:	There''s no place like 127.0.0.1, the "localhost"

Apenas Isso Não Basta

Apenas escapar caracteres não é suficiente, uma vez que não existem apenas strings. Também temos dados numéricos, como inteiros, floats e outros tipos de ponto flutuante, que não são envolvidos por aspas em consultas SQL.

Considere a seguinte SQL:

$sql = "SELECT id, nome, sobrenome FROM autores WHERE id=" . $id;

Se $id tiver o valor 0; DROP TABLE autores; —, a SQL final será:

SELECT id, nome, sobrenome FROM autores WHERE id=0; DROP TABLE autores; --;

Isso removeria a tabela “autores”.

A solução é, novamente, muito simples: basta fazer casting, ou coerção, convertendo o parâmetro para um tipo numérico.

No exemplo acima, bastaria isto:

$id = (int) $id;

O mesmo vale para float, double e os demais tipos de dados.

A Solução Ideal: Prepared Statements

Existe uma forma ideal de resolver o problema de SQL Injection: Prepared Statements.

O que são Prepared Statements?

Prepared Statements são úteis para executar uma mesma consulta diversas vezes, com parâmetros distintos, de forma eficiente.

Porém também há outra utilidade: filtragem nativa de consultas, a fim de evitar SQL Injection. Isso dá mais segurança ao seu sistema.

Nesse tipo de consulta, os parâmetros não são enviados diretamente na consulta. Eles são enviados em um “pacote” separado ao SGBD. O SGBD, por sua vez, é quem faz a associação entre string SQL e parâmetros.

Em outras palavras, não vamos colocar variáveis diretamente na consulta.

É possível usar Prepared Statements com as extensões mais recentes do PHP, como a MySQLi. Recomendo utilizar PDO, que permite abstrair o banco de dados.

6. Conclusão

SQL Injection é um problema muito grave, que muitos programadores iniciantes deixam passar despercebido, principalmente por falta de conhecimento.

Apesar disso, sua prevenção é muito simples. Basta entender o funcionamento do ataque para saber como se defender dele.

Facebook Twitter Linkedin Digg Delicious Reddit Stumbleupon Tumblr Posterous