Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 utilizado en el AccountController predeterminado para ASP.NET MVC 1.0 y ASP.NET MVC 2 es vulnerable a los 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 comprender la vulnerabilidad, veamos cómo funciona el redireccionamiento de inicio de sesión en un proyecto de aplicación web de MVC 2 predeterminado ASP.NET. 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 querystring returnUrl para que el usuario pueda devolverse a la dirección URL solicitada originalmente después de haber iniciado sesión 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.
Figura 01: Página de inicio de sesión con una redirección abierta
Dado que el parámetro querystring ReturnUrl no se valida, un atacante puede modificarlo para insertar cualquier dirección URL en el parámetro para realizar un ataque de redireccionamiento abierto. 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 iniciar sesión en un sitio web específico, lo que nos hace vulnerable a un ataque de suplantación de identidad (phishing). 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
Tenga en cuenta que la URL de retorno apunta a nerddiner.com, a la que le falta una "n" de 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.
Figura 02: Página de inicio de sesión de NerdDinner con una redirección abierta
Cuando iniciamos sesión correctamente, la acción LogOn de ASP.NET MVC AccountController nos redirige a la dirección URL especificada en el parámetro querystring 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. Qué torpes, debemos de haber escrito mal la contraseña.
Figura 03: Pantalla de inicio de sesión de NerdDinner falsificado
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 nuestro nombre de usuario y contraseña, y no somos conscientes de que se 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.
Listado 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().
Listado 2: acción de inicio de sesión 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.
Listado 3: el método IsLocalUrl() del ASP.NET MVC 3 UrlHelper 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.
Enumeración 4: método IsUrlLocalToHost() de la clase System.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 la URL frente al 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. En el código siguiente se muestra el método IsLocalUrl() modificado para su uso con una clase de controlador en ASP.NET aplicaciones MVC 1.0 y 2.
Lista 5: método IsLocalUrl(), que se modifica para su uso con una clase de 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"
}
}
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: método LogOn actualizado 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.
Figura 04: Probar la acción logOn actualizada
Después de iniciar sesión correctamente, se redirige a la acción Inicio/Controlador de índice en lugar de a la dirección URL externa.
Figura 05: Ataque de redireccionamiento abierto 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 agregar este código con alguna modificación a ASP.NET aplicaciones 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.