Partilhar via


Impedir ataques de redirecionamento aberto (C#)

por Jon Galloway

Este tutorial explica como você pode evitar ataques de redirecionamento aberto em seus aplicativos ASP.NET MVC. Este tutorial discute as alterações que foram feitas no AccountController no ASP.NET MVC 3 e demonstra como você pode aplicar essas alterações em seus aplicativos MVC 1.0 e 2 ASP.NET existentes.

O que é um ataque de redirecionamento aberto?

Qualquer aplicativo Web que redirecione para uma URL especificada por meio da solicitação, como a querystring ou os dados de formulário, pode ser potencialmente adulterado para redirecionar os usuários para uma URL externa e mal-intencionada. Essa violação é chamada de ataque de redirecionamento aberto.

Sempre que a lógica do aplicativo for redirecionada para uma URL especificada, você deverá verificar se a URL de redirecionamento não foi adulterada. O logon usado no AccountController padrão para ASP.NET MVC 1.0 e ASP.NET MVC 2 é vulnerável a ataques de redirecionamento abertos. Felizmente, é fácil atualizar seus aplicativos existentes para usar as correções da versão prévia do ASP.NET MVC 3.

Para entender a vulnerabilidade, vamos examinar como o redirecionamento de logon funciona em um projeto de aplicativo Web ASP.NET MVC 2 padrão. Neste aplicativo, a tentativa de visitar uma ação do controlador que tenha o atributo [Autorizar] redirecionará usuários não autorizados para a exibição /Account/LogOn. Esse redirecionamento para /Account/LogOn incluirá um parâmetro querystring returnUrl para que o usuário possa ser retornado à URL solicitada originalmente depois de ter feito logon com êxito.

Na captura de tela abaixo, podemos ver que uma tentativa de acessar o modo de exibição /Account/ChangePassword quando não está conectado resulta em um redirecionamento para /Account/LogOn? ReturnUrl=%2fAccount%2fChangePassword%2f.

Captura de tela que mostra a página Logon do Aplicativo My M V C. A barra de título está realçada.

Figura 01: Página de logon com um redirecionamento aberto

Como o parâmetro querystring ReturnUrl não é validado, um invasor pode modificá-lo para injetar qualquer endereço de URL no parâmetro para realizar um ataque de redirecionamento aberto. Para demonstrar isso, podemos modificar o parâmetro ReturnUrl para https://bing.com, portanto, a URL de logon resultante será /Account/LogOn? ReturnUrl=https://www.bing.com/. Após fazer logon com êxito no site, somos redirecionados para https://bing.com. Como esse redirecionamento não é validado, ele pode apontar para um site mal-intencionado que tenta enganar o usuário.

Um ataque de redirecionamento aberto mais complexo

Ataques de redirecionamento aberto são especialmente perigosos porque um invasor sabe que estamos tentando entrar em um site específico, o que nos torna vulneráveis a um ataque de phishing. Por exemplo, um invasor pode enviar emails mal-intencionados para usuários do site na tentativa de capturar suas senhas. Vamos ver como isso funcionaria no site nerddinner. (Observe que o site nerddinner ao vivo foi atualizado para proteger contra ataques de redirecionamento aberto.)

Primeiro, um invasor nos envia um link para a página de logon no NerdDinner que inclui um redirecionamento para sua página forjada:

http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn

Observe que a URL de retorno aponta para nerddiner.com, que está faltando um "n" da palavra jantar. Neste exemplo, esse é um domínio que o invasor controla. Quando acessamos o link acima, somos levados para a página de logon NerdDinner.com legítima.

Captura de tela que mostra a home page do Nerd Dinner dot com. A barra de título é realçada e preenchida com a U R L que aponta para Nerd Diner dot com.

Figura 02: página de logon do NerdDinner com um redirecionamento aberto

Quando fazemos logon corretamente, a ASP.NET ação LogOn do AccountController do MVC nos redireciona para a URL especificada no parâmetro querystring returnUrl. Nesse caso, é a URL que o invasor inseriu, que é http://nerddiner.com/Account/LogOn. A menos que estejamos extremamente atentos, é muito provável que não percebamos isso, especialmente porque o invasor tem o cuidado de garantir que sua página forjada se pareça exatamente com a página de logon legítima. Esta página de logon inclui uma mensagem de erro solicitando que façamos logon novamente. Desajeitado, devemos ter digitado incorretamente nossa senha.

Captura de tela mostrando a página Nerd Dinner Log On forjada, solicitando que o usuário reinsira suas credenciais. A URL forjada na barra de título está realçada.

Figura 03: Tela de logon do NerdDinner forjada

Quando digitamos novamente nosso nome de usuário e senha, a página de logon forjada salva as informações e nos envia de volta para o site NerdDinner.com legítimo. Neste ponto, o site de NerdDinner.com já nos autenticou, portanto, a página de logon forjada pode redirecionar diretamente para essa página. O resultado final é que o invasor tem nosso nome de usuário e senha e não sabemos se o fornecemos a ele.

Examinando o código vulnerável na ação AccountController LogOn

O código para a ação LogOn em um aplicativo ASP.NET MVC 2 é mostrado abaixo. Observe que, após um logon bem-sucedido, o controlador retorna um redirecionamento para o returnUrl. Você pode ver que nenhuma validação está sendo executada no parâmetro returnUrl.

Listagem 1 – ASP.NET ação de LogOn do MVC 2 no AccountController.cs

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (!String.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

Agora, vamos examinar as alterações na ação ASP.NET LogOn do MVC 3. Esse código foi alterado para validar o parâmetro returnUrl chamando um novo método na classe auxiliar System.Web.Mvc.Url chamada IsLocalUrl().

Listagem 2 – ASP.NET ação logOn do MVC 3 em AccountController.cs

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", 
        "The user name or password provided is incorrect.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

Isso foi alterado para validar o parâmetro de URL de retorno chamando um novo método na classe auxiliar System.Web.Mvc.Url, IsLocalUrl().

Protegendo seu ASP.NET aplicativos MVC 1.0 e MVC 2

Podemos aproveitar as alterações do ASP.NET MVC 3 em nossos aplicativos MVC 1.0 e 2 ASP.NET existentes adicionando o método auxiliar IsLocalUrl() e atualizando a ação LogOn para validar o parâmetro returnUrl.

O método UrlHelper IsLocalUrl() na verdade apenas chamando um método em System.Web.WebPages, pois essa validação também é usada por aplicativos Páginas da Web do ASP.NET.

Listagem 3 – O método IsLocalUrl() do ASP.NET UrlHelper do MVC 3 class

public bool IsLocalUrl(string url) {
    return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(
        RequestContext.HttpContext.Request, url);
}

O método IsUrlLocalToHost contém a lógica de validação real, conforme mostrado na Listagem 4.

Listagem 4 – Método IsUrlLocalToHost() da classe System.Web.WebPages RequestExtensions

public static bool IsUrlLocalToHost(this HttpRequestBase request, string url)
{
   return !url.IsEmpty() &&
          ((url[0] == '/' && (url.Length == 1 ||
           (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
           (url.Length > 1 &&
            url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
}

Em nosso aplicativo ASP.NET MVC 1.0 ou 2, adicionaremos um método IsLocalUrl() ao AccountController, mas você será incentivado a adicioná-lo a uma classe auxiliar separada, se possível. Faremos duas pequenas alterações no ASP.NET versão MVC 3 de IsLocalUrl() para que ele funcione dentro do AccountController. Primeiro, vamos alterá-lo de um método público para um método privado, pois os métodos públicos em controladores podem ser acessados como ações do controlador. Em segundo lugar, modificaremos a chamada que verifica o host da URL em relação ao host do aplicativo. Essa chamada usa um campo RequestContext local na classe UrlHelper. Em vez de usar isso. RequestContext.HttpContext.Request.Url.Host, usaremos isso. Request.Url.Host. O código a seguir mostra o método IsLocalUrl() modificado para uso com uma classe de controlador no ASP.NET aplicativos MVC 1.0 e 2.

Listagem 5 – Método IsLocalUrl(), que é modificado para uso com uma classe controlador MVC

private bool IsLocalUrl(string url)
{
   if (string.IsNullOrEmpty(url))
   {
      return false;
   }
   else
   {
      return ((url[0] == '/' && (url.Length == 1 ||
              (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
              (url.Length > 1 &&
               url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
   }
}

Agora que o método IsLocalUrl() está em vigor, podemos chamá-lo de nossa ação LogOn para validar o parâmetro returnUrl, conforme mostrado no código a seguir.

Listagem 6 – Método LogOn atualizado que valida o parâmetro returnUrl

[HttpPost] 
public ActionResult LogOn(LogOnModel model, string returnUrl) 
{ 
    if (ModelState.IsValid) 
    { 
        if (MembershipService.ValidateUser(model.UserName, model.Password)) 
        { 
            FormsService.SignIn(model.UserName, model.RememberMe); 
            if (IsLocalUrl(returnUrl)) 
            { 
                return Redirect(returnUrl); 
            } 
            else 
            { 
                return RedirectToAction("Index", "Home"); 
            } 
        } 
        else 
        { 
            ModelState.AddModelError("", 
            "The user name or password provided is incorrect."); 
        } 
    }
}

Agora podemos testar um ataque de redirecionamento aberto tentando fazer logon usando uma URL de retorno externa. Vamos usar /Account/LogOn? ReturnUrl=https://www.bing.com/ novamente.

Captura de tela que mostra a página Logon do Aplicativo My M V C. A barra de título é realçada e preenchida com a URL de retorno externo.

Figura 04: Testando a ação atualizada do LogOn

Depois de fazer logon com êxito, somos redirecionados para a ação Controlador de Página Inicial/Índice em vez da URL externa.

Captura de tela que mostra a página Índice do Aplicativo My M V C.

Figura 05: Ataque de redirecionamento aberto derrotado

Resumo

Ataques de redirecionamento aberto podem ocorrer quando URLs de redirecionamento são passadas como parâmetros na URL de um aplicativo. O modelo ASP.NET MVC 3 inclui código para proteger contra ataques de redirecionamento aberto. Você pode adicionar esse código com alguma modificação ao ASP.NET aplicativos MVC 1.0 e 2. Para proteger contra ataques de redirecionamento aberto ao fazer logon em aplicativos ASP.NET 1.0 e 2, adicione um método IsLocalUrl() e valide o parâmetro returnUrl na ação LogOn.