Dando continuação ao artigo sobre Segurança em Sistemas de login, hoje mostrarei como se defender de ataques por SQL Injection. Foco MySQL e PostgreSQL, os principais SGBDs gratuitos da atualidade.
Caso não tenha visto a primeira parte do artigo, leia-a aqui:
Segurança em SIstemas de Login: Senhas e Cookies
Sumário
1. Introdução
2. Magic Quotes
3. Soluções Especificas para cada SGBD
3.1. MySQL
3.2. PostgreSQL
3.3. Exemplos de códigos para MySQL e PostgreSQL
4. Apenas Isso Não Basta
5. A Solução Ideal: Prepared Statements
6. Conclusão
1. 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çãoo é 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.
2. 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 contruir 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 previnir injeção de SQL. Os desenvolvedores de hoje estão mais a par de segurança e acabam usando mecanismos especificos 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
3. 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. Leia mais sobre isso neste link.
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" |
4. 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.
5. 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. Leia mais sobre isso neste meu artigo sobre PDO e MySQL.
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.
Roberto Beraldo
Latest posts by Roberto Beraldo (see all)
- Não Tenha Preguiça de Ler! - 25/04/2016
- Como Atualizar Scripts PHP de MySQL Para MySQLi - 29/10/2015
- Como usar PDO com banco de dados MySQL - 10/09/2015