Portuguese English German

CSRF - O que é

Cross-Site Request Forgery (CSRF) é uma das vulnerabilidades mais conhecidas e perigosas em aplicações web. Neste artigo vamos aprender como ela funciona e como prevení-la. Discutiremos as várias abordagens modernas de mitigação e suas diferenças.

Cenário Didático

Imagine o seguinte cenário:

  1. Você tem uma conta no banco AmigoDaGrana;
  2. Você costuma fazer pagamentos pelo internet banking, isto é, pagar suas contas pela internet, usando o site do AmigoDaGrana;
  3. Um certo dia você resolve entrar na sua conta do banco para ver seu saldo;
  4. Você descobre que seu saldo é R$ 100,00;
  5. Ainda estando logado no site do banco (http://banco.com.br), você abre uma nova aba e acessa o site http://malicioso.com.br;
  6. Após o site http://malicioso.com.br ter sido carregado, você retorna à aba do banco e descobre que seu saldo agora é R$ 70,00.

Nesse cenário, você possívelmente foi uma vítima de um ataque que explora a vulnerabilidade de Cross-Site Request Forgery (CSRF). Vamos entender o que aconteceu por trás das cortinas, a começar pelo nome dessa vulnerabilidade.

Significado de "Cross-Site Request Forgery"

"Cross-Site" se refere a uma vulnerabilidade que é explorada entre sites, ou seja, pelo nome é possível identificar que no mínimo dois (2) sites estão envolvidos na exploração. "Request" significa requisição e "Forgery" forjar. Logo, "request forgery" significa forjar uma requisição, uma requisição fajuta.

Juntando todas essas palavras podemos resumir essa vulnerabilidade como uma requisição forjada por um atacante em um site (nesse caso http://malicioso.com.br) para afetar outro site (nesse caso o banco). O atacante força o navegador do usuário a fazer uma requisição para o site do banco como se o próprio usuário a tivesse feito. Nesse caso a requisição significa a transferência dos R$ 30 que sumiram.

Em termos mais nerds, podemos dizer que essa vulnerabilidade trata-se então de uma exploração da confiança do servidor no navegador do usuário. O servidor parte da premissa que as requisições são sempre iniciadas pelo usuário, quando na verdade podem ser iniciadas por outros sites. Em nosso exemplo, a requisição foi iniciada por http://malicioso.com.br.

Adentrando o tecniquês

Nesse cenário que usamos como exemplo, podemos notar que R$ 30,00 sumiram de nossa conta. Esse dinheiro que "sumiu" na verdade se transformou em um pagamento de conta ou em uma transferência para algum indivíduo ou alguma empresa. Vamos supor que olhando o extrato, identificamos que foi feita uma transferência de R$ 30,00 para João Hacker.

Antes de entender como a transferência para João Hacker aconteceu, precisamos entender como uma transferência normal e autêntica funciona. Vamos simular um passo-a-passo:

1) Usuário loga no banco e ganha cookie contendo identificador de sessão

CSRF - Login

Um pouco sobre o Identificador de Sessão...

Se vários clientes acessam o banco ao mesmo tempo, como o banco sabe diferenciar cada um? Ele fornece para usuário um identificador de sessão. Esse identificador geralmente é composto por muitas letras e números, como por exemplo: 1F9fNJIKSAGFj130imk013mVNH. Esse identificador é geralmente armazenado dentro de um Cookie, que é um arquivo temporário que contém poucos dados e fica sobre a guarda do navegador, e passado durante toda a comunicação feita do navegador do cliente para o banco.

...Voltando

2) Usuário acessa a página /transferência e recebe o formulário para preencher

CSRF - Formulário de Transferência

3) Usuário envia o formulário de transferência preenchido

CSRF - Realiza Transferência

Certo, entendemos como uma transferência normal funciona. Agora, pensando como um atacante, precisamos apenas criar uma forma de fazer o navegador do usuário criar a requisição de transferência, mas especificando a conta do atacante. A requisição ficaria assim:

CSRF - Realiza Transferência Maliciosa

Porém temos um problema. Como conseguiremos, como atacantes, obter o valor cookie de sessão? É aí que está o pulo do gato. Não precisamos saber qual o valor do cookie de sessão, porque sempre que o navegador faz requisições para um determinado domínio (banco.com.br), ele sempre envia todos os cookies que estão cadastrados para aquele domínio.

Nós como atacantes não conseguiremos identificar qual o valor desse cookie e nem precisamos. O navegador vai executar a requisição fraudulenta que construímos passando o cookie de sessão para nós. O navegador nem sabe que está sendo explorado. Essa "inocência" por parte do navegador é um problema conhecido na área de computação chamado: "The Confused Deputy Problem".

Criando nosso exploit

Exploit é o termo utilizado para denominar os programas que exploram vulnerabilidades de maneira automatizada, isto é, o código que vamos criar para explorar a vulnerabilidade de CSRF no site do banco se chama exploit. Para complementar esta explicação, podemos também dizer que o site http://malicioso.com.br contém um exploit.

Para criar nosso exploit, precisamos entender o ecossistema ao redor dele.

Repare que até o momento entendemos a requisição que temos que fazer o navegador enviar, mas ainda não chegamos na parte prática, ou seja, como o navegador vai de fato criar essa requisição. Note que esse ataque precisa ser feito no mesmo navegador, porque o cookie de sessão fica armazenado nos arquivos referentes a cada navegador. Se o usuário estiver logado no banco pelo Internet Explorer e executar nosso exploit no Google Chrome o ataque não vai funcionar.

Veja abaixo o código HTML presente em http://malicioso.com.br:

<html>
    <body>
        <img width="1" height="1" src="http://banco.com.br/processaTransferencia?conta_destino=5678&valor=30.00" />
    </body>
</html>

Que estranho. Uma tag "img"? Essa tag não é usada apenas para mostrar imagens? Em termos de visualização, sim. Porém na prática o que importa para gente é que o navegador faz uma requisição (GET) para o endereço "http://banco.com.br/transferencia?conta_destino=5678&valor=30.00". Não estamos nem aí para a resposta. O que importa é que o navegador faça a requisição. Se der certo saberemos olhando o extrato da nossa conta de atacante (conta do João Hacker).

Juntando tudo

CSRF - Big Picture

Dúvidas comuns

1) Se a tag <img> faz apenas requisições GET, basta modificarmos a página de transferência do banco para receber requições POST ao invés de GET. Assim as requisições GET, como esse exemplo da imagem não funcionarão mais, certo? Errado! É só criarmos um exploit para requisições POST e está resolvido. Veja o exemplo abaixo:

<html>
    <!-- jQuery é uma biblioteca Javascript famosa -->
    <!-- Ela facilita muito a vida, por exemplo na -->
    <!-- Criação de requisições Ajax (nosso caso) -->
    <script src="jquery.min.js"></script>
    <script>
        $.ajax({
          type: "POST",
          url: "http://banco.com.br/processaTransferencia",
          data: "conta_destino=5678&valor=30.00"
        });
    </script>
</html>

2) Mas o Cross-Origin Resource Sharing (CORS) vai prevenir que essa requisição seja feita. Talvez. Essa resposta é mais complexa e requer um entendimento sobre a "Same-Origin Policy" e sobre o CORS. Vamos lá:

Introdução à "Same-Origin Policy"...

Visando proteger que as páginas web acessem dados sensíveis de outras páginas web, criaram a Same-Origin Policy, que nada mais é que uma política de segurança implementada por cada navegador. Essa política coloca certas restrições em como as páginas web de origens diferentes (por exemplo domínios diferentes como banco.com.br e malicioso.com.br) podem interagir.

Porém algumas vezes essas aplicações querem interagir e ambas autorizam que essa comunicação seja feita. Para essa autorização ser reconhecida pelo navegador, é necessário que essa autorização seja implementada pelo Cross-Origin Resource Sharing (CORS), cuja explicação está logo abaixo:

Introdução ao CORS...

Cross-Origin Resource Sharing (CORS) foi criado para liberar certas restrições aplicadas pela Same-Origin Policy com base em critérios específicos.

Quando o navegador detecta uma requisição entre origens diferentes, ele primeiro identifica se a requisição é segura para ser enviada.

Para verificar se é segura, ele verifica se a requisição está sendo enviada usando o método GET, POST ou HEAD e se não existem cabeçalhos incomuns, por exemplo "X-JWT". Se por algum motivo a requisição tiver outra configuração que não a mencionada, o navegador é então forçado a enviar uma "Preflight Request".

A "Preflight Request", que significa "Requisição antes do vôo", é uma requisição que usa o método OPTIONS e passa alguns cabeçalhos extras, como "Origin", "Access-Control-Request-Method" e "Access-Control-Request-Headers" e é enviada ao servidor sem nenhum corpo. Por exemplo:

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

O servidor deve então responder aceitando a origem, o método e os headers explicitamente pelos cabeçalhos "Access-Control-Allow-Origin", "Access-Control-Allow-Method" e "Access-Control-Allow-Headers". Por exemplo:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

Feito isso o navegador entende que a requisição é segura para ser enviada e prossegue com o envio, permitindo também que a recém autorizada origem tenha acesso aos dados.

...Voltando à dúvida

É claro que o banco jamais vai liberar a origem do site malicioso.com.br, então o site malicioso.com.br vai se preocupar em evitar a "preflight request".

Acontece que se o banco fizer requisições para a página de transferência passando cabeçalhos incomuns ou métodos incomuns, a requisição que será lançada pelo exploit não vai funcionar. Isso é comum quando o banco faz o processamento usando uma API que recebe apenas conteúdo JSON, obrigando o navegador a mudar o tipo de conteúdo da requisição (cabeçalho Content-Type) para "application/json". Isso já configura uma requisição não segura para o navegador e exige uma "preflight request".

Se por outro lado a requisição já for considerada segura, o exploit vai conseguir lançar a requisição com sucesso, mas não conseguirá obter os dados da resposta dessa requisição. Mas como já falamos acima, não nos importamos com a resposta. O importante é lançar a requisição, portanto o CORS pode ajudar ou não a prevenir ataques de CSRF dependendo de como as requisições do frontend do banco acessam o backend do próprio banco.

Mitigação

A essência do ataque é simples, mas como podemos ver existem muitos detalhes que influenciam o ataque e consequentemente a defesa também. Vamos entender algumas estratégias que podem ser abordadas para prevenir a exploração de CSRF. Já adianto que essas estratégias são divididas em ações a serem tomadas pelos usuários ou pelos desenvolvedores de sites.

Mitigação por parte do usuário

Lembram do cenário do banco? Pois bem, se o usuário não estivesse logado no banco, o ataque não seria possível, certo? Sim, o ataque não seria possível, porque o usuário estar logado é um pré-requisito.

Mas essa não é a única precaução que um usuário pode ter. É possível fazer uso de plugins como o noScript que tenta bloquear essas requisições antes delas acontecerem. O problema é que geralmente o noScript vai além e bloqueia requisições demais e acaba quebrando os sites, tornando a navegação muito ruim. Pelo menos foi minha impressão no passado. Contudo, a idéia que um plugin possa interferir e identificar requisições entre sites que sejam possivelmente maliciosas é interessante sim.

Outra abordagem ainda é utilizar navegadores diferentes. Por exemplo ter o hábito de acessar o banco usando um navegador apenas e outro navegador para suas demais necessidades pode ser uma boa estratégia para prevenir a exploração de ataques CSRF.

Mitigação por parte dos desenvolvedores

Não dá para contar que todos os usuários vão se precaver, certo? Por isso temos sempre que nos preocupar com os ataques CSRF como desenvolvedores.

Se revisarmos como o ataque CSRF acontece, vamos notar que o servidor não consegue distinguir quando a requisição é legítima ou quando ela é forjada. A mitigação do CSRF se concentra em aplicar essa validação para o servidor e existem várias formas de fazer isso.

Mitigação com Nonce

Number Used Once (Nonce) significa "número utilizado uma vez apenas". Essa é uma das formas de proteção contra ataques CSRF. Ela se resume em adicionar uma validação antes do servidor executar a determinada operação, por exemplo transferência de valores no banco.com.br.

O servidor envia o código nonce para a página que contém o formulário de transferência e espera receber o mesmo token igualzinho. O segredo desse controle está no fato de que o site atacante (malicioso.com.br) não sabe qual é esse token. Desde que esse token não seja fácil de adivinhar, é claro. Porque se por exemplo o token for "123", sempre sendo composto por 3 dígitos, é só testarmos todas as possibilidades [0-9][0-9][0-9] e rapidamente adivinhamos o token.

Repare que usei a palavra token ao invés de nonce. Fiz isso porque nonce significa que o token será usado uma única vez, sendo necessário gerar um outro nonce para a próxima operação, quando o token não possui essa restrição.

Se o usuário tiver um token único por sessão já é suficiente. Não é necessário gerar um novo token para cada operação, apesar de ser interessante do ponto de vista de segurança, acaba dificultando a manutenção dos tokens e também atrapalha a navegação de usuários que estão acessando o site por múltiplos dispositivos ou mesmo múltiplas abas no navegador. Por exemplo ao fazer a transferência em uma aba, ao tentar realizar uma outra operação na segunda aba vai falhar, se o usuário não tiver renovado o token.

É importante lembrar que o token deve também ser único por usuário, ser difícil de ser adivinhado e ser gerado usando um gerador de números randômicos criptográficos seguro, caso contrário será possível prever qual será o próximo token. Por fim, nada disso adianta se o servidor não negar requisições onde o token não possua o valor esperado.

Mitigação com Double Submit Cookies

Antes de falarmos de "Double Submit Cookies" ou "Triple Submit Cookies", precisamos falar um pouco sobre como os cookies funcionam:

Introdução aos Cookies...

Os cookies podem ser criados tanto pelo navegador, utilizando a linguagem JavaScript, como pelo servidor, ao passar o cabeçalho "Set-Cookie" na resposta HTTP. Neste cabeçalho, o servidor informa obrigatoriamente o nome e o valor do cookie, mas pode ir além.

É possível adicionar a flag HttpOnly, que permite que o cookie seja apenas trafegado em requisições HTTP, impossibilitando a sua leitura por javascript por meio da instruções document.cookie. Existe também a flag Secure, que força o cookie a trafegar apenas por conexões HTTPS ao invés de HTTP e HTTPS.

Além das flags é possível especificar o domínio que o cookie é válido e o caminho também, por exemplo o cookie pode funcionar apenas para o caminho /transferencia.

Tem também o atributo "SameSite" que é uma das estratégias de mitigação que serão abordadas nas próximas linhas :)

Exemplo de cabeçalho Set-Cookie:

Set-Cookie: name=Anderson; domain=banco.com.br; path=/transferencia; Secure; HttpOnly

...Voltando ao "Double Submit Cookies"

Basicamente a idéia do Double Submit Cookies é enviar um cookie com valor aleatório e um parâmetro no corpo da mensagem contendo esse mesmo valor aleatório. O servidor deve então verificar se o valor que veio do cookie é o mesmo valor presente no parâmetro enviado no corpo da requisição.

Esse modelo é stateless, porque diferentemente da solução de Nonce/Token o servidor não precisa saber o segredo de antemão, e também é eficaz porque o atacante não vai conseguir ler o valor do cookie para replicá-lo no parâmetro da requisição HTTP.

Ele pode ser inclusive melhorado se "amarrarmos o ID de sessão" junto aos valores do double submit para garantir que não seja possível burlar esse controle apenas enviando valores aleatórios, mesmo que idênticos, no cookie e no corpo da requisição.

O problema é se o atacante tiver acesso a algum subdomínio (de maneira autêntica ou por meio de Cross-Site Scripting), por exemplo malicioso.banco.com.br, que consiga ler os cookies de banco.com.br. Desta forma o atacante conseguirá ler o cookie e utilizar o mesmo valor no parâmetro enviado no corpo da requisição. Cookies podem ser escritos por ataques "Man-In-The-Middle" e "Man-In-The-Browser" também.

Dado a fraqueza desse controle mencionada acima, sugeriram o mecanismo de mitigação chamado "Triple Submit Cookies", mas que é passível a bypass também. Inclusive há uma análise interessante feita aqui e a apresentação do Triple Submit aqui.

Mitigação com atributo "SameSite" do Cookie

Recentemente introduzido no Google Chrome 51, o atributo "SameSite" informa o navegador que os cookies só devem ser enviados ao servidor se a requisição partir da mesma origem que os criou.

Isso faz com que as chamadas de "malicioso.com.br" para "banco.com.br" não incluam nenhum cookie! O ideal é que todos os seus cookies usem essa flag por padrão, assim como as flag HttpOnly e Secure. Elas devem ser desabilitadas apenas se tiverem uma boa justificativa para abrir mão da segurança.

Essa proteção se aplica até para as requisições consideradas 'seguras' pelo navegador quando falamos de CORS. Até as requisições GET estarão protegidas.

Seria sensacional se essa proteção fosse adicionada aos cookies por padrão, mas por ser nova, se isso acontecesse, muitos sites da Internet iriam parar de funcionar no Google Chrome e iriam mudar para os navegadores que não possuem esse controle, como o Firefox por exemplo. Isso poderia até acarretar na volta do Internet Explorer .... só que não.

Mitigação com controles alternativos

Outros controles que não tokens, nem combinação de cookies com corpo da resposta ajudam a prevenir ataques que exploram o CSRF. São eles:

  • Validação do cabeçalho "Referer": Sim, é simples e ajuda bastante. Um atacante não consegue modificar o cabeçalho "Referer". Uma tentativa de ataque de CSRF vai trazer na requisição que o site que originou a requisição é "malicioso.com.br" e não "banco.com.br";
  • Verificar o cabeçalho "Origin": Parecido com o "Referer", mas diferente. O cabeçalho Origin foi criado para prevenir ataques entre domínios e, diferentemente do "Referer", o cabeçalho Origin é enviado mesmo em requisições HTTP originadas de URLs em HTTPS;
  • Adicionar um Captcha: Visto que o Captcha mitiga ataques automatizados, ele também serve para bloquear ataques CSRF, porque o Captcha tem que ser preenchido corretamente.

Dúvidas comuns

1) Estou desenvolvendo uma API, logo tive que desabilitar a proteção de CSRF do meu framework para fazê-la funcionar. Por ser uma API imagino que não tem problema, certo? Errado! Esse é um dos erros mais comuns dos desenvolvedores por subestimarem o estrago que pode ser feito por ataques CSRF. Mesmo sendo uma API, você consegue guardar o identificador da sessão no cookie ou mesmo no localStorage (um espaço reservado para cada domínio armazenar mais informações que um cookie; utiliza o formato "chave => valor" e foi introduzido no HTML 5 com limite de até 5 MB) e passar como um cabeçalho HTTP. Comumente usado no cabeçalho "Authorization", mas você pode criar outro, como o "X-JWT" (onde cabeçalhos iniciados com X significam que são customizados, isto é, fora da especificação do protocolo HTTP) para passar um JSON Web Token (em inglês) por exemplo. Entretanto armazenar o identificar de sessão no localStorage traz o risco abaixo (dúvida #2):

Um pouco mais sobre o localStorage...

O localStorage, que significa "armazenamento local", foi introduzido no HTML 5 e tem como objetivo prover um espaço maior do que os cookies para as aplicações lerem e armazenarem valores arbitrários sem tempo de expiração. Ele funciona com chaves e valores, por exemplo a aplicação pode guardar a chave "user" com o seguinte valor:

{
  id: 10,
  name: "Anderson Dadario",
  plan: {
    name: "Basic",
    price: {
      amount: 10,
      currency: "USD"
    }
  }
}

Existem outras formas de armazenamento além do localStorage como o sessionStorage, Indexed DB, Web SQL Database, etc, mas não serão abordados aqui.

2) Estou armazenando o ID de sessão no localStorage. Estou safo? Ainda não. Você pode dizer que está prevenido contra ataques CSRF sim, porque o navegador só envia os cookies do domínio por padrão e os dados do localStorage não, mas por outro lado uma nova vulnerabilidade foi criada na sua aplicação.

O seu identificador de sessão que antes poderia estar protegido no Cookie contra leitura pela linguagem JavaScript utilizando o atributo "Http Only", agora está passível a ser lido pela linguagem JavaScript, visto que tal proteção não existe para o localStorage. Em outras palavras um ataque de Cross-Site Scripting (XSS) pode acessar o localStorage, obter o identificador de sessão e enviar para o servidor de um atacante, que por sua vez pode sequestrar a conta do usuário logado.

Conclusão

Como podem ver, a essência do CSRF (também chamado de XSRF), é simples, mas existe uma camada de complexidade para ataques e defesas mais sofisticadas, além das múltiplas abordagens.

Esse ataque consegue causar, e tem causado, um estrago muito grade ao impersonar as ações dos usuários e geralmente é apresentada com severidade técnica alta em relatórios de teste de intrusão.

Se você gostou desse artigo, por favor compartilhe e se inscreva na lista abaixo para receber no seu inbox os meus futuros artigos. Em breve terei mais cursos online disponíveis que vão incluir exemplos práticos de ataque e defesa de CSRF e de muitas outras vulnerabilidades.

Isso é tudo, pessoal.

Obrigado e até breve :)

Share on Twitter Share on Facebook Share on Google Plus Share on LinkedIn Share on Hacker News

Postagens Populares

Newsletter


Twitter