Segurança com PHP VI – CSRF

Olá meu grandes leitores que desejam tornar suas aplicações mais seguras! Dando seguimento a série de artigos sobre segurança, hoje falarei do CSRF.

Veja o primeiro artigo: Segurança com PHP I – SQL Injection

Veja o segundo artigo: Segurança com PHP II – PHP Injection

Veja o terceiro artigo: Segurança com PHP III – XSS

DEFINIÇÃO

CSRF (Cross Site Request Forgeries), tradução Falsificação de solicitação entre sites, é um tipo de exploração maliciosa de um website/aplicação pelo qual comandos não autorizados são transmitidos de um usuário que confia no website. Fonte: Wikipedia

Em outras palavras, é um ataque que “força” um usuário à executar ações indesejadas em uma aplicação web. Um CSRF bem aplicado, pode comprometer os dados e funcionamento do usuário caso o mesmo seja um usuário comum ao sistema, quiçá, se o mesmo for um usuário administrador ou root.

CENÁRIO

Vamos imaginar um portal muito visitado e que tenha muita repercussão em tudo o que publicado, como notícias, fotos, anunciantes e dentre outros. Existirá nele um painel administrativo protegido por uma sessão onde existe uma área para publicações. Vamos usar de exemplo de pedido para salvar uma notícia:

GET http://www.portalmuitochique.com.br/noticias/postar.php
HTTP/1.1 .........
titulo=T%C3%ADtulo%20da%20not%C3%ADcia&corpo=Corpo%20da%20not%C3%ADcia&autor=Autor%20da%20Not%C3%ADcia

Traduzindo: titulo=Título da notícia&corpo=Corpo da notícia&autor=Autor da Notícia

Toda vez que for enviado um GET ou POST para este recurso(URL) e o usuário estiver autenticado na aplicação, o mesmo deve entender que é para ser publicado uma notícia no nosso portal.

FORMA DE ATAQUE

Nessa web de meu Deus, geralmente essas requisições GET ou POST são feitas por formulários, como o formulário abaixo:

<form action="http://www.portalmuitochique.com.br/noticias/postar.php" method="get">
    <p>
        <label for="titulo">Título:</label>
	<input type="text" name="titulo" />
    </p>
    <p>
        <label for="autor">Autor:</label>
	<input type="text" name="autor" />
    </p>
    <p>
        <label for="corpo">Corpo:</label>
        <textarea name="corpo" cols="20" rows="10"></textarea>
    </p>
    <p><button type="submit">Salvar Notícia</button></p>
</form>

Captura de Tela 2016-04-26 às 12.14.52

TÁ, MAS ATÉ AGORA NÃO VI NENHUM PROBLEMA EM RELAÇÃO A SEGURANÇA.

Muita calma jovem guerreiro, você já pensou que esse mesmo formulário possa estar em outro servidor, nas mãos de qualquer outra pessoa? #OMG

Se atacante submeter esse formulário o sistema receberá o GET e irá salvar a notícia no portal.

OPA, ALTO LÁ! SE O ATACANTE NÃO ESTIVER AUTENTICADO, COMO É QUE SERÁ SALVO A NOTÍCIA NO PORTAL?

Simples, uma técnica de raro conhecimento e de alta tecnologia chamada: E-mail  =)

Existe uma técnica muito conhecida que é incorporar uma imagem(<img>) de zero byte no corpo do e-mail. Como abaixo:

<img alt="" width="1" height="1" border="0" src="http://www.portalmuitochique.com.br/noticias/postar.php?titulo=T%C3%ADtulo%20da%20not%C3%ADcia&corpo=Corpo%20da%20not%C3%ADcia&autor=Autor%20da%20Not%C3%ADcia"/>

Se essa tag de imagem for incluída no e-mail, a vítima só verá uma caixinha indicando que o navegador não pode processar a imagem. No entanto, o navegador continua a enviar a solicitação para www.portalmuitochique.com.br sem qualquer indicação visual do processo.

É por isso que nos clientes de e-mail de hoje quando recebem uma imagem no corpo dos e-mails, sempre perguntam aos usuários se realmente desejam processar as imagens vindas do remetente, veja um exemplo do GMail:

Captura de Tela 2016-04-26 às 12.18.53

MACACOS ME MORDAM, EU SEMPRE MANDO EXIBIR AS IMAGENS! E AGORA, COMO FAÇO PARA ME PREVENIR DISTO?

Existem várias técnicas para tratar esse tipo de ataque, ensinarei uma forma bem simples e direta. Vamos supor que o script para salvar a notícia (postar.php) seja este:

<?php

session_start();

// ... Rotinas para verificar a autenticidade ...

if(isset($_POST['titulo'])) {
	
	// Publicando a noticia
	$noticia = new Noticia();
	$noticia->setTitulo($_POST['titulo']);
	$noticia->setAutor($_POST['autor']);
	$noticia->setCorpo($_POST['corpo']);
	$noticia->publicarNoticia();
	
}

Desta forma o atacante poderá utilizar aquela técnica da imagem de zero byte. Vamos refatorar este nosso script para evitar o ataque CSRF vamos fazer o seguinte:

  1. Gerar um token (ou hash) randômico, que fosse renovado em um determinado tempo;
  2. Colocar este token em uma variável de sessão;
  3. Colocar este token em um campo escondido(hidden) no seu formulário;
  4. Ao submeter seu formulário fazer a comparação do campo escondido com a variável de sessão;

Vamos primeiramente criar um simples script chamado gerar_token_csrf.php para gerar o token:

<?php

if(!isset($_SESSION))
	session_start();

if(!isset($_SESSION['csrf_token']))
	$_SESSION['csrf_token'] = sha1(uniqid(rand(), true));

Entendido os passos, vamos ao código! Vamos fazer uma pequena modificação no nosso formulário:
<?php include 'gerar_token_csrf.php' ?>

<form action="http://www.portalmuitochique.com.br/noticias/postar" method="post">
	<input type="hidden" name="csrf_token" id="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?>" />
	<p>
		<label for="titulo">Título:</label>
		<input type="text" name="titulo" />
	</p>
	<p>
		<label for="autor">Autor:</label>
		<input type="text" name="autor" />
	</p>
	<p>
		<label for="corpo">Corpo:</label>
		<textarea name="corpo" cols="20" rows="10"></textarea>
	</p>
	<p><button type="submit">Salvar Notícia</button></p>
</form>

E por último, mas não menos importante, a comparação dos 2 tokens. Vamos alterar o postar.php:
<?php

include('gerar_token_csrf.php');

// ... Rotinas para verificar a autenticidade ...

if(isset($_GET['csrf_token'])) {

	if($_GET['csrf_token'] != $_SESSION['csrf_token']) {
		echo "Tokens não validados!";
		die();
	}
	
	// Publicando a noticia
	$noticia = new Noticia();
	$noticia->setTitulo($_POST['titulo']);
	$noticia->setAutor($_POST['autor']);
	$noticia->setCorpo($_POST['corpo']);
	$noticia->publicarNoticia();
	
}

Agora sim! Como sempre haverá uma chave grande, única e dinâmica o atacante até poderá enviar o mesmo GET ou POST com o parâmetro “csrf_token”, mas a probabilidade dele acertar esse token é quase zero!

Existem outras formas mais rigorosas para evitar este ataque, como por exemplo: antes de verificar os 2 tokens, poderia ser verificado também de onde veio a requisição.

Bom geralerinha, é isso! Mais um tipo de ataque desvendado.

Um forte abraço a todos e até a próxima!