O Cross-site request forgery (em português, falsificação de solicitação entre sites) que chamarei a partir de agora de CSRF, se trata de uma vulnerabilidade encontrada em sites e navegadores. Através desses sites vulneráveis, é possível enviar comandos e solicitações não autorizadas entre sites.
Um CSRF pode ocorrer de diversas formas e neste artigo vamos tratar a solicitação de informações via POST entre sites. Uma das formas de evitar o CSRF é criar campos escondidos com valores aleatórios dentro de cada formulário, como mostra o exemplo abaixo:
<input name="__RequestVerificationToken" type="hidden" value="O1IxOVF3szSWDqYMQsZftmk8qvWz4HnNg/KOZ7Y9SvgzfsqN0b8gwIFlziju5oXRF5pjKzR/Ne+zlp+B0eHE0fPDOiR5ORKKlzuapfCDN8liJEIGGdfbgWeZQgU6P2CDsJuKxYhEK2OTlWtfaJNaz70bFklSpk0cbTmqm3N/Kk0=" />
Desta forma, a aplicação relaciona este valor ao usuário que utiliza aplicação para verificar se a próxima requisição foi originada por ele. Sendo assim, a aplicação verifica se o token existe e se é o do usuário a cada publicação. Ainda existem os que guardam este informação em cookie, o que não é recomendado devido aos problemas de segurança que apresenta.
1. Utilizando o Helper Html.AntiForgeryToken()
No MVC 3 foi introduzido o Helper Html.AntiForgeryToken(), o qual faz a validação das chamadas criando campos ocultos dentro do formulário como dito anteriormente. Segue exemplo de campo criado.
Para utilizarmos este Helper, devemos fazer a chamada dele dentro do form, segue exemplo de código.
@using (Html.BeginForm("Register", "Mailling", FormMethod.Post)) { @this.Html.AntiForgeryToken() }
Com isso, temos o seguinte HTML.
Assim com o campo oculto, o Html.AntiForgeryToken() ira criar um cookie para o “__RequestVerificationToken” com o mesmo valor do campo “__RequestVerificationToken”.
Após criarmos a chamada do Helper na View, devemos informar a Action do Controller que deve ser validado o Token, para isso tempos o atributo “ValidateAntiForgeryToken”.
Com isso, o MVC 3 entende que toda chamada a este Action deve ser validado o Token. Com isso, temos o seguinte código:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Register() { return View(); }
Confirma as ações que o MVC 3 efetua:
- Cria um cookie chamado de __RequestVerificationToken;
- Cria um Request.Form chamado de __RequestVerificationToken;
- Validação se os valores são iguais.
Caso os valores não sejam iguais, é apresentada uma página de erro (caso os erros não forem tradados pela aplicação). Segue exemplo de pagina com o erro.
1.1. Utilizando Salt
Uma forma de criar validações mais seguras e especificadas para cada formulário é utilizar o Salt. O Salt se trata de uma “string” no qual irá modificar o valor do “__RequestVerificationToken”. Para utilizar, basta passar uma string na chamada do Token, segue exemplo de código:
Html.AntiForgeryToken("umaStringQualquer")
E no nosso action, devemos informar a mesma string. Segue exemplo de código:
ValidateAntiForgeryToken(Salt="umaStringQualquer")
Desta forma, temos um valor mais específico para cada formulário e também mais seguro.
1.2. Problemas de segurança
Como foi dito, mais seguro e não completamente seguro, pois, mesmo gerando um Token diferente para cada formulário, ainda é possível se roubar o Token. O que fazemos aqui é dificultar o trabalho do cracker.
Para isso, basta apenas olhar o código fonte e copiar o valor do “__RequestVerificationToken” e utilizá-lo em sua solicitação própria, por exemplo.
2. Bloqueando publicações de outros sites
Para exemplificar melhor a falha de segurança CSRF, vamos criar duas aplicações simples (portanto, desconsiderem arquitetura, performanc, etc nesse caso), uma que se chama “MvcApplicationXSS” que possui a vulnerabilidade e a “MvcApplicationHack” que explora esta vulnerabilidade.
2.1. Aplicação MvcApplicationXSS
Nessa aplicação, vamos criar um Model bem simples para representar o cadastro do Mailling. Segue o código da classe:
public class MaillingModel { private int mallingId = 0; private string name = string.Empty; private string email = string.Empty; public MaillingModel() { // Crio um ID aletatório this.MallingId = this.CreateId(); } public int MallingId { get { return this.mallingId; } set { this.mallingId = value; } } [ DisplayName("Nome"), Required(ErrorMessage = "Nome é um campo obrigatório") ] public string Name { get { return this.name; } set { this.name = value; } } [ DisplayName("E-mail"), Required(ErrorMessage = "E-mail é um campo obrigatório") ] public string Email { get { return this.email; } set { this.email = value; } } private int CreateId() { Random objRandom = new Random(); return objRandom.Next(10000, 90000); } public void SaveDB() { string serverPath = HttpContext.Current.Server.MapPath("~/App_Data/mailling_" + this.MallingId + ".txt"); File.Create(serverPath).Close(); StreamWriter sw = File.AppendText(serverPath); sw.WriteLine(string.Format("ID:{0} Name:{1} Email: {2}", this.mallingId, this.Name, this.Email)); sw.Close(); } }
Conforme o código, criamos uma classe de MaillignModel com as propriedade do ID, Nome e E-mail e mais o método “SaveDB”, que vai simular o cadastro no banco de dados (mas, para simplificar o exemplo, apenas vamos gerar um arquivo .txt na pasta “App_Data”).
Feito isso, vamos criar o nosso controller chamado o MaillingController. Segue o código:
[HttpGet] public ActionResult Register() { return View(); } [HttpPost] public ActionResult Register(MaillingModel mailling) { if (ModelState.IsValid) { mailling.SaveDB(); } return View(); }
Nesse controller, criamos dois Action com o nome “Register”, um para o GET e outro para a publicação que recebe o objeto MaillingModel como parâmetro e efetua o cadastro no banco de dados.
Agora vamos criar a View para o action do “Register”.
@{ ViewBag.Title = "Register"; Layout = "~/Views/Shared/_Layout.cshtml"; } <h1> Cadastro de Mailling </h1> @model MvcApplicationXSS.Models.MaillingModel @using (Html.BeginForm("Register", "Mailling", FormMethod.Post)) { <fieldset> <legend> Dados </legend> @Html.LabelFor(model => model.Name) @Html.TextBoxFor(model => model.Name, new { @maxlength = "50", @size = "50" }) <br /> @Html.LabelFor(model => model.Email) @Html.TextBoxFor(model => model.Email, new { @maxlength = "50", @size = "50" }) </fieldset> <input type="submit" value="Cadastrar" /> @Html.ValidationSummary() }
Ao executarmos a aplicação, temos o seguinte formulário:
Preenchemos os dados, e submetemos o formulário.
Após submeter o formulário, foi criado o arquivo “mailling_51893.txt” na pasta “App_Data” com os seguintes dados:
ID:51893 Name:Carlos Eduardo Email: carlos@teste.com
Agora, vamos criar a aplicação “MvcApplicationHack” que vai explorar a vulnerabilidade.
2.2. Aplicação MvcApplicationHack
Após criada a solution, vamos criar um controller chamado HackController. Segue o código.
public class HackController : Controller { public ActionResult Mailling() { return View(); } }
Nesse Controller, criamos a Action com o nome Mailling e agora vamos criar sua View.
Após criada a View, precisamos dos dados do formulário de cadastro de mailing. Para isso, basta exibirmos o código fonte, copiar o trecho de código referente ao formulário e colar na View. Segue código:
<h1>Cadastro de Mailling</h1> <form action="/Mailling/Register" method="post"> <fieldset> <legend>Dados</legend> <label for="Name">Nome</label> <input data-val="true" data-val-required="Nome &#233; um campo obrigat&#243;rio" id="Name" maxlength="50" name="Name" size="50" type="text" value="" /> <br /> <label for="Email">E-mail</label> <input data-val="true" data-val-required="E-mail &#233; um campo obrigat&#243;rio" id="Email" maxlength="50" name="Email" size="50" type="text" value="" /> </fieldset> <input type="submit" value="Cadastrar" /> <div data-valmsg-summary="true"><ul><li style="display:none"></li> </ul></div> </form>
Agora vamos alterar algumas informações importantes, como o action do formulário, para submeter ao formulário de cadastro do mailling e remover propriedades “data-val” e “data-val-required” e o “validation-summary” para deixar o código mais limpo. Alterei também o H1 e Legend para que nas visualizações fosse mais fácil de identificar cada página. Segue o código final:
<h1>Hack Mailling</h1> <form action="http://localhost:3434/Mailling/Register" method="post"> <fieldset> <legend>Dados Cross-Site</legend> <label for="Name">Nome</label> <input id="Name" maxlength="50" name="Name" size="50" type="text" /> <br /> <label for="Email">E-mail</label> <input id="Email" maxlength="50" name="Email" size="50" type="text" /> </fieldset> <input type="submit" value="Cadastrar" /> </form>
Feito isso, vamos rodar a aplicação “MvcApplicationXSS” e a “MvcApplicationHack”. Vamos até o formulário da “MvcApplicationHack” preencher os dados e submeter.
Feito isso, ao submeter o site, foi criada o seguinte a arquivo “mailling_17119.txt”na pasta “App_Data” com as seguintes informações.
Podemos ver que a aplicação “MvcApplicationXSS” aceitou uma solicitação da aplicação “MvcApplicationHack” sem fazer nenhum questionamento. Caso você tenha a curiosidade, coloque um breakpoint dentro da action POST do Register da solução “MvcApplicationXSS”, você verá que ao submeter novamente o formulário da aplicação “MvcApplicationHack” o mesmo para no breakpoint confiome figura baixo.
Agora, pense no que pode ser feito em sua aplicação, como por exemplo, o cracker poderia criar um submit via javascript ou mesmo C# para ficar cadastrando dados em seu banco de dados, até mesmo fazer o DoS (Negação de Serviço) estourando o tamanho do seu banco de dados ou por consumir muito recurso do seu servidor. Esses são exemplos simplistas, mas, a muito a ser explorado com isso.
Mas, como podemos bloquear esse tipo de solicitação de forma “simples” e rápida e verificar a origem das solicitações. Para isso, vamos voltar a solução “MvcApplicationXSS” e abrir o arquivo “Global.asax”. Após aberto, verifique se existe a chamado do método“Application_BeginRequest”, caso não, crie você mesmo ele. Segue código:
protected void Application_BeginRequest(Object sender, EventArgs e) { }
O método “Application_BeginRequest” trata todas as requisições de arquivos da aplicação, logo, tudo que for solicitado, passara por este método e é isso o que precisamos: verificar todas as solicitações que ocorrem em nossa aplicação. Agora, vamos adicionar o seguinte trecho de código dentro do método “Application_BeginRequest”:
if (Request.UrlReferrer != null && Request.UrlReferrer.Authority.Equals(Request.Url.Authority) == false) { Response.End(); }
Primeira coisa que verificamos é se o “UrlReferrer” não é nulo. A propriedade “UrlReferrer” traz informações sobre a URL anterior a URL atual. Exemplo: Se estamos na URL www.exemplo.com.br/Cadastrar e fazermos um submit para a URL www.exemplo.com.br/CadastroEfetuado, quando verificarmos a UrlReferrer na página “CadastroEfetuado” terá os valores referentes a URL “Cadastrar”. Sendo assim, toda vez que o “UrlReferrer” não for null, então ele é uma solicitação de outra página.
Após verificarmos se a “UrlReferrer” não é nulo, verificamos se a propriedade “Authority” da “UrlReferrer” é igual a “Authority” da “Url” atual. Se verificarmos os valores, notamos que os valores são diferentes, na “Authority” da “UrlReferrer” temos o valor “localhost:666” é na “Authority” da “Url” atual temos o valor “localhost:3434”.
Apesar de serem apenas as portas, se as aplicações estiverem instaladas em servidores a própria Url seria diferente. Desta forma, toda vez que ocorrer essa tentativa de CSRF, o sistema vai parar a execução da página através do comando “Request.End()”. Segue código final do método:
protected void Application_BeginRequest(Object sender, EventArgs e) { // Verifico se a URL anterior é diferente da Url atual if (Request.UrlReferrer != null && Request.UrlReferrer.Authority.Equals(Request.Url.Authority) == false) { // Se forem diferentes, paro a execução da página Response.End(); } }
Mais coisas poderiam ser feitas além de parar a execução do “Request”, como guardar o IP e, caso isso ocorra diversas vezes, bloquear o IP e/ou efetuar log dessas ocorrências para avaliação posterior.
Agora, coloque um breakpoint dentro da action POST do “Register” da solução “MvcApplicationXSS”, você verá que ao submeter novamente o formulário da aplicação “MvcApplicationHack” o mesmo não para no breakpoint.
Desta forma, criamos um bloqueio para as solicitações feitas de outras aplicações e fazendo com que ela aceite apenas solicitações internas. Mas vale lembrar que essa não é a solução definitiva, pois, os cabeçalhos HTTP podem ser criados manualmente, assim um cracker poderia alterar as informações do cabeçalho HTTP e sua aplicação entenderia que é uma solicitação interna, mas isso é assunto para outro artigo. Lembre-se que segurança é um conjunto de soluções, não existe uma única ação que resolva tudo, não existe bala de prata.
Agora ficamos por aqui. Sugestões, dúvidas e criticas são bem vindas!