DevSecOps

9 nov, 2012

preMVC3 – Cross-site request forgery: evitando post

Publicidade

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:

  1. Cria um cookie chamado de __RequestVerificationToken;
  2. Cria um Request.Form chamado de __RequestVerificationToken;
  3. 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 &amp;#233; um campo obrigat&amp;#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 &amp;#233; um campo obrigat&amp;#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!

Segue link para baixar as 2 soluções.