Compartir vía


Prevenir los ataques de redireccionamiento abierto (C#)

por Jon Galloway

En este tutorial se explica cómo puede evitar ataques de redirección abierta en las aplicaciones de ASP.NET MVC. En este tutorial se describen los cambios realizados en AccountController en ASP.NET MVC 3 y se muestra cómo puede aplicar estos cambios en las aplicaciones existentes de ASP.NET MVC 1.0 y 2.

¿Qué es un ataque de redirección abierta?

Cualquier aplicación web que redirija a una dirección URL especificada a través de la solicitud, como la cadena de consulta o los datos de formulario, se puede alterar para redirigir a los usuarios a una dirección URL externa malintencionada. Esta manipulación se denomina ataque de redireccionamiento abierto.

Siempre que la lógica de la aplicación redirija a una dirección URL especificada, debe comprobar que la dirección URL de redireccionamiento no se ha alterado. El inicio de sesión usado en AccountController predeterminado para ASP.NET MVC 1.0 y ASP.NET MVC 2 es vulnerable a ataques de redirección abierta. Afortunadamente, es fácil actualizar las aplicaciones existentes para usar las correcciones de la versión preliminar de ASP.NET MVC 3.

Para entender la vulnerabilidad, veamos cómo funciona la redirección de inicio de sesión en un proyecto de aplicación web ASP.NET MVC 2 predeterminada. En esta aplicación, al intentar visitar una acción de controlador que tenga el atributo [Authorize] se redirigirá a los usuarios no autorizados a la vista /Account/LogOn. Esta redirección a /Account/LogOn incluirá un parámetro de cadena de consulta returnUrl para que el usuario pueda ser devuelto a la URL solicitada originalmente una vez que se haya conectado correctamente.

En la captura de pantalla siguiente, podemos ver que un intento de acceder a la vista /Account/ChangePassword cuando no haya iniciado sesión da como resultado una redirección a /Account/LogOn? ReturnUrl=%2fAccount%2fChangePassword%2f.

Screenshot that shows the My M V C Application Log On page. The title bar is highlighted.

Figura 01: Página de inicio de sesión con una redirección abierta

Dado que el parámetro de cadena de consulta ReturnUrl no está validado, un atacante puede modificarlo para introducir cualquier dirección URL en el parámetro y llevar a cabo un ataque de redirección abierta. Para demostrar esto, podemos modificar el parámetro ReturnUrl en https://bing.com, por lo que la dirección URL de inicio de sesión resultante será /Account/LogOn? ReturnUrl=https://www.bing.com/. Después de iniciar sesión correctamente en el sitio, se redirige a https://bing.com. Dado que no se valida esta redirección, podría apuntar a un sitio malintencionado que intente engañar al usuario.

Un ataque de redirección abierta más complejo

Los ataques de redirección abierta son especialmente peligrosos porque un atacante sabe que estamos intentando entrar en un sitio web específico, lo que nos hace vulnerables a un ataque de suplantación de identidad. Por ejemplo, un atacante podría enviar correos electrónicos malintencionados a los usuarios del sitio web en un intento de capturar sus contraseñas. Veamos cómo funcionaría esto en el sitio NerdDinner. (Tenga en cuenta que el sitio NerdDinner activo se ha actualizado para protegerse frente a ataques de redirección abierta).

En primer lugar, un atacante nos envía un vínculo a la página de inicio de sesión de NerdDinner que incluye una redirección a su página falsificada:

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

Observe que la URL de retorno apunta a nerddiner.com, al que le falta una "n" en la palabra dinner. En este ejemplo, se trata de un dominio que controla el atacante. Cuando accedemos al vínculo anterior, se nos lleva a la página de inicio de sesión NerdDinner.com legítima.

Screenshot that shows the Nerd Dinner dot com Home page. The title bar is highlighted and filled with the U R L that points to Nerd Diner dot com.

Figura 02: Página de inicio de sesión de NerdDinner con una redirección abierta

Cuando iniciamos la sesión correctamente, la acción LogOn del controlador de cuenta ASP.NET MVC nos redirige a la dirección URL especificada en el parámetro de cadena de consulta returnUrl. En este caso, es la dirección URL especificada por el atacante, que es http://nerddiner.com/Account/LogOn. A menos que seamos extremadamente atentos, es muy probable que no notemos esto, especialmente porque el atacante ha sido cuidadoso para asegurarse de que su página falsificada es exactamente similar a la página de inicio de sesión legítima. Esta página de inicio de sesión incluye un mensaje de error que solicita que iniciemos sesión de nuevo. Torpes de nosotros, hemos debido de teclear mal nuestra contraseña.

Screenshot showing the forged Nerd Dinner Log On page, prompting the user to reenter their credentials. The forged U R L in the title bar is highlighted.

Figura 03: Pantalla de inicio de sesión de NerdDinner falsificada

Cuando se vuelve a escribir el nombre de usuario y la contraseña, la página de inicio de sesión falsificada guarda la información y nos envía de vuelta al sitio legítimo NerdDinner.com. En este momento, el sitio de NerdDinner.com ya nos ha autenticado, por lo que la página de inicio de sesión falsificada puede redirigir directamente a esa página. El resultado final es que el atacante tiene el nombre de usuario y la contraseña, y no sabemos que los hemos proporcionado.

Examinar el código vulnerable en la acción AccountController LogOn

A continuación se muestra el código de la acción LogOn en una aplicación de ASP.NET MVC 2. Tenga en cuenta que, tras un inicio de sesión correcto, el controlador devuelve un redireccionamiento al returnUrl. Puede ver que no se realiza ninguna validación en el parámetro returnUrl.

Lista 1: Acción LogOn de ASP.NET MVC 2 en 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);
}

Ahora echemos un vistazo a los cambios realizados en la acción LogOn de ASP.NET MVC 3. Este código se ha cambiado para validar el parámetro returnUrl llamando a un nuevo método en la clase auxiliar System.Web.Mvc.Url denominada IsLocalUrl().

Lista 2: Acción LogOn de ASP.NET MVC 3 en 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);
}

Esto se ha cambiado para validar el parámetro de dirección URL de retorno llamando a un nuevo método en la clase auxiliar System.Web.Mvc.Url, IsLocalUrl().

Protección de las aplicaciones ASP.NET MVC 1.0 y MVC 2

Podemos aprovechar los cambios de ASP.NET MVC 3 en nuestras aplicaciones existentes ASP.NET MVC 1.0 y 2 agregando el método auxiliar IsLocalUrl() y actualizando la acción LogOn para validar el parámetro returnUrl.

El método UrlHelper IsLocalUrl() simplemente llama a un método en System.Web.WebPages, ya que esta validación también la usan las aplicaciones de páginas web de ASP.NET.

Lista 3: El método IsLocalUrl() del UrlHelper de ASP.NET MVC 3 class

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

El método IsUrlLocalToHost contiene la lógica de validación real, como se muestra en la lista 4.

Lista 4: Método IsUrlLocalToHost() de la clase 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"
}

En nuestra aplicación ASP.NET MVC 1.0 o 2, agregaremos un método IsLocalUrl() al AccountController, pero se recomienda agregarlo a una clase auxiliar independiente si es posible. Realizaremos dos pequeños cambios en la versión ASP.NET MVC 3 de IsLocalUrl() para que funcione dentro del AccountController. En primer lugar, lo cambiaremos de un método público a un método privado, ya que se puede tener acceso a los métodos públicos de los controladores como acciones de controlador. En segundo lugar, modificaremos la llamada que comprueba el host de dirección URL en el host de la aplicación. Esa llamada usa un campo RequestContext local en la clase UrlHelper. En lugar de usar this.RequestContext.HttpContext.Request.Url.Host, usaremos this.Request.Url.Host. El siguiente código muestra el método IsLocalUrl() modificado para su uso con una clase de controlador en aplicaciones ASP.NET MVC 1.0 y 2.

Lista 5: Método IsLocalUrl(), que se modifica para su uso con una clase MVC Controller

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"
   }
}

Ahora que el método IsLocalUrl() está en vigor, podemos llamarlo desde nuestra acción LogOn para validar el parámetro returnUrl, como se muestra en el código siguiente.

Lista 6: Actualización del método LogOn que valida el 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."); 
        } 
    }
}

Ahora podemos probar un ataque de redireccionamiento abierto intentando iniciar sesión con una dirección URL de retorno externa. Vamos a usar /Account/LogOn? ReturnUrl=https://www.bing.com/ de nuevo.

Screenshot that shows the My M V C Application Log On page. The title bar is highlighted and filled with the external return U R L.

Figura 04: Prueba de la acción LogOn actualizada

Tras iniciar sesión correctamente, se nos redirige a la acción Inicio/Controlador de índice en lugar de a la URL externa.

Screenshot that shows the My M V C Application Index page.

Figura 05: Ataque de redirección abierta derrotado

Resumen

Los ataques de redirección abierta pueden producirse cuando las direcciones URL de redireccionamiento se pasan como parámetros en la dirección URL de una aplicación. La plantilla ASP.NET MVC 3 incluye código para protegerse frente a ataques de redirección abierta. Puede añadir este código con algunas modificaciones a las aplicaciones ASP.NET MVC 1.0 y 2. Para protegerse contra ataques de redireccionamiento abiertos al iniciar sesión en aplicaciones de ASP.NET 1.0 y 2, agregue un método IsLocalUrl() y valide el parámetro returnUrl en la acción LogOn.