Feed e Twitter

Feed RSS Twitter

Busca

Por Lustosa em 22/04/2009 às 21:13

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.

O problema

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

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.

Forma 1: addslashes() no PHP

É 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.

Forma 2: deixar o próprio banco escapar

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.

Forma 3: 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!

Conclusão

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.

Artigos relacionados

Arquivado em programação, segurança

Feed RSS para os comentários deste artigo.


3 comentários em “Evitando SQL injection em PHP”

  1. Jan Seidl comentou:

    É, 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!

  2. Bruno comentou:

    Ó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!!!

  3. André comentou:

    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/


Copyright 2009 Ataraxia!   Sinopse