Em um mundo ideal, todos os desenvolvedores, ao programar seus aplicativos em qualquer linguagem, deveriam se preocupar com segurança, em especial com a validação dos dados. No PHP, isso não é diferente.
Uma classe de problemas de segurança bem específica do ambiente web é a chamada SQL Injection, literalmente uma “Injeção de SQL”. E embora a solução seja simples, é preciso que o programador preste atenção, pra evitar que seu aplicativo acabe sendo explorado por uma falha de segurança como esta.
Digamos que na página principal do nosso site, a gente tenha um formulário de login mais ou menos assim:
<form action="login.php" method="post"> Login: <input type="text" name="login" /> Senha: <input type="password" name="senha" /> <input type="submit" /> </form>
Um formulário bem simples. E para tratar este formulário, temos o login.php, que vai checar em um banco de dados se o login e a senha estão corretos, da seguinte forma (colocarei apenas o trecho com a consulta SQL relevante):
<?php $sql = "SELECT * FROM usuarios WHERE login = '{$_POST['login']}' AND senha = '{$_POST['senha']}'"; $res = mysql_query($sql); if (mysql_num_rows($res) > 0) { print "Login OK"; } else { print "Login e senha não conferem"; } ?>
Ou seja, verificamos na tabela usuarios se temos uma linha com o login e a senha fornecidos. Caso essa consulta retorne alguma coisa, significa que existe uma linha com o login e a senha fornecidos, caso contrário, não existe.
Se no formulário eu preencho o login com “joe” e a senha com “xxxx”, a seguinte consulta SQL será montada e executada:
SELECT * FROM usuarios WHERE login = 'joe' AND senha = 'xxxx'
Porém, o que acontece se eu preecher o formulário com o login = joe, e a senha com: xxxx’ OR true –
Neste caso, teremos a seguinte consulta:
SELECT * FROM usuarios WHERE login = 'joe' AND senha = 'xxxx' OR TRUE --'
Essa consulta vai retornar todas as linhas do banco de dados (por causa do “OR true” no final), e como a checagem no código é se a consulta retornou mais de uma linha, estaremos logados.
Existem formas mais sofisticadas de proceder com este tipo de ataque, mas o foco do artigo é na defesa.
FIEO é uma técnica bastante usada para aumentar a segurança dos aplicativos web. Significa “Filter Input/Escape Output”, ou traduzindo, “Filtre a Entrada/Escape a Saída”.
O primeiro passo é a filtragem da entrada, já que a regra número 1 da segurança em aplicativos web diz que não podemos jamais confiar em informações vindas de fontes externas. No nosso caso, poderíamos fazer a filtragem do login e da senha checando se eles possuem apenas caracteres permitidos. Por exemplo, o site poderia ter uma regra especificando que o login deve conter apenas letras e números. A filtragem então, poderia ser feita usando a função ctype_alnum():
<?php if (!ctype_alnum($_POST['login'])) { die("login possúi caracteres inválidos"); } ?>
Se o FI do FIEO lida com a filtragem da entrada, o EO lida com a manutenção da saída no contexto em que for lida.
Ou seja, FI garante que a nossa entrada é valida, e EO garante que quem quer que receba os nossos dados, irá interpretá-los da forma correta.
No nosso caso, é importante que as aspas simples não sejam interpretadas como delimitadores na consulta SQL. E se eu quisesse ter uma senha que tivesse uma aspa simples no meio?
Existem 3 formas clássicas de se resolver este problema.
É a forma mais simples (e ingênua) de se tratar o problema. Por exemplo, se queremos nos proteger das aspas simples, poderíamos simplesmente escapá-las no PHP da seguinte forma:
<?php $login = addslashes($_POST['login']); $senha = addslashes($_POST['senha']); ?>
Dessa forma, caso uma aspa simples chegasse do usuário, seria devidamente escapada com uma “\” antes, e o servidor SQL não iria interpretá-la como um delimitador.
Para o nosso exemplo de ataque, resolveria.
O grande problema com esta solução é que ela só prevê ataques que usem aspas simples. Bancos de dados diferentes podem usar caracteres de controle diferentes, como aspas duplas, quebras de linha, ou outras coisas mais estranhas. E checar por todas estas formas é longe de ser a maneira mais eficaz (e viável!) de se proceder.
Temos uma outra opção, suportada por vários bancos de dados.
A segunda opção é passar a string para o banco e deixar ele se virar. Afinal, ele sabe o que pode e o que não pode entrar em uma string. No MySQL, isso poderia ser feito da seguinte forma:
<?php $login = mysql_real_escape_string($_POST['login']); $senha = mysql_real_escape_string($_POST['senha']); ?>
Segundo a documentação, chamando esta função, o banco escapa automaticamente as seguintes sequencias: \x00, \n, \r, \, ‘, ” e \x1a. Ou seja, pelo visto as aspas simples não eram nosso único problema mesmo.
Existem funções semelhantes para outros bancos de dados (por exemplo, pg_escape_string() para o PostgreSQL).
Porém, a solução que considero mais elegante é usar consultas preparadas.
A idéia de se preparar uma consulta é dizer ao banco de dados o formato da consulta (apenas os parâmetros formais), e fazer a amarração dos parâmetros reais somente na hora da execução.
A maior parte dos bancos de dados suportam consultas preparadas, mas vou mostrar o exemplo usando MySQL, devido a popularidade.
<?php $dbh = new mysqli("localhost", "ususario", "senha", "banco"); $c = $dbh->prepare("SELECT * FROM usuarios WHERE login = ? AND senha = ?"); $c->bind_param("login", $_POST['login']); $c->bind_param("senha", $_POST['senha']); $c->execute(); ?>
Ou seja, primeiro informamos ao banco qual será a consulta a ser executada. Perceba que usamos interrogações no lugar dos parâmetros reais. Com isso, o banco prepara a consulta pra ser executada.
Logo em seguida, dizemos ao banco o valor que cada parâmetro irá receber. Esta etapa se chama amarração (binding). Como o banco de dados sabe o tipo de cada campo, ele também sabe como proceder pra escapar corretamente cada valor.
E por último, executamos a consulta. Simples, fácil e seguro!
SQL Injection é um problema sério que afeta muitos aplicativos web por aí afora, porém é um problema que só existe por causa da ingenuidade dos programadores, que não programam pensando em segurança. A solução, como vimos, é bem simples.
Feed RSS para os comentários deste artigo.
April 23rd, 2009 às 20:05
É, apesar do magic_quotes segurar a onda não segura tudo! mysql_real_escape_string ou uso de preparadores de query (comum em frameworks) é sempre uma boa nesse caso!
Ótimo artigo!
June 14th, 2009 às 22:32
Ótimo artigo.
Fica aqui uma dica de fazer mais artigos sobre segurança na internet. Temos que difundir mais a segurança nos sites, mesmo os mais simples.
Parabéns!!!
August 27th, 2009 às 16:01
Olá, ótimo post
Eu postei um sistema de proteção tambem contra sql inject, porem usa uma função com varios metodos.(não foi criado por mim)
considero muito segura, “TEM Q SER” pois sql inject é um dos meio mais faceis de invadir um website.
Está ai o link:
http://www.gasparimsat.com/index.php/23/04/2009/se-proteja-do-sql-inject/