Entendiendo la falsificación de solicitudes entre sitios en .NET
() translation by (you can also view the original English article)
Solo puedes producir aplicaciones web seguras si tienes en cuenta la seguridad desde el principio. Esto requiere pensar en las posibles formas en que alguien podría atacar tu sitio a medida que creas cada página, formulario y acción. También requiere comprender los tipos más comunes de problemas de seguridad y cómo abordarlos.
El tipo más común de agujero de seguridad en una página web permite que un atacante ejecute comandos en nombre de un usuario, pero el usuario no lo sabe. El ataque de falsificación de solicitudes entre sitios explota la confianza que un sitio web ya ha establecido con el navegador web de un usuario.
En este tutorial, hablaremos sobre qué es un ataque de falsificación de solicitud entre sitios y cómo se ejecuta. Luego, crearemos una aplicación ASP.NET MVC simple que sea vulnerable a este ataque y arreglaremos la aplicación para evitar que vuelva a suceder.
¿Qué es la falsificación de solicitudes entre sitios?
El ataque de falsificación de solicitud entre sitios asume primero que la víctima ya se ha autenticado en un sitio web de destino, como un sitio bancario, Paypal u otro sitio para ser atacado. Esta autenticación debe almacenarse de tal manera que, si el usuario abandona el sitio y regresa, el sitio web de destino lo sigue viendo como conectado. El atacante debe hacer que la víctima acceda a una página o enlace que ejecutará una solicitud o una publicación en el sitio web de destino. Si el ataque funciona, el sitio web de destino verá una solicitud proveniente de la víctima y ejecutará la solicitud como ese usuario. Esto, en efecto, permite al atacante ejecutar cualquier acción deseada en el sitio web objetivo haciéndose pasar por la víctima. El resultado potencial podría transferir dinero, restablecer una contraseña o cambiar una dirección de correo electrónico en el sitio web de destino.
¿Cómo funciona el ataque?
El acto de hacer que la víctima use un enlace no requiere que haga clic en un enlace. Un simple enlace de imagen podría ser suficiente:
1 |
<img src="http://www.examplebank.com/movemoney.aspx?from=myaccount&to=youraccount&amount=1000.00" width="1" height="1" /> |
Incluir un enlace como este en una publicación de foro, comentario de blog o sitio de redes sociales aparentemente inofensivos podría sorprender al usuario. Los ejemplos más complejos usan JavaScript para crear una solicitud de publicación HTTP completa y enviarla al sitio web de destino.
Desarrollando una aplicación web vulnerable en ASP.NET MVC
Creemos una aplicación ASP.NET MVC simple y dejémosla vulnerable a este ataque. Usaré Visual Studio 2012 para estos ejemplos, pero esto también funcionará en Visual Studio 2010 o Visual Web Developer 2010 funcionará si has instalado el soporte para MVC 4 que puedes descargar e instalar desde Microsoft.



Empieza por crear un nuevo proyecto y elige utilizar la plantilla Proyecto de Internet. Cualquiera de los motores de vista funcionará, pero aquí usaré el motor de vista ASPX.
Agregaremos un campo a la tabla UserProfile para almacenar una dirección de correo electrónico. En Explorador de servidores, expande Conexiones de datos. Deberías ver la Conexión predeterminada creada con la información para los inicios de sesión y las membresías. Haz clic con el botón derecho en la tabla UserProfile y haz clic en Abrir definición de tabla. En la línea en blanco debajo de la tabla UserName, agregaremos una nueva columna para el correo electrónico. Nombra la columna emailaddress
, dale el tipo nvarchar(MAX)
y marca la opción Permitir que sea null. Ahora haz clic en Actualizar para guardar la nueva versión de la tabla.
Esto nos da una plantilla básica de una aplicación web, con soporte de inicio de sesión, muy similar a lo que muchos desarrolladores empezarían al intentar crear una aplicación. Si ejecutas la aplicación ahora, verás que se muestra y funciona. Presiona F5 o usa DEBUG -> Iniciar Debugging desde el menú para abrir el sitio web.



Creemos una cuenta de prueba que podamos usar para este ejemplo. Haz clic en el enlace Registrarse y crea una cuenta con el nombre de usuario y la contraseña que desees. Aquí voy a usar una cuenta llamada testuser
. Después de la creación, verás que ahora inicié sesión como usuario de prueba o "testuser". Una vez hecho esto, agreguemos una página a esta aplicación para permitir que el usuario cambie su correo electrónico.



Antes de crear esa página para cambiar la dirección de correo electrónico, primero debemos hacer un cambio en la aplicación para que el código sea consciente de la nueva columna que acabamos de agregar. Abre el archivo AccountModels.cs
en la carpeta Models
y actualiza la clase UserProfile
para que coincida con lo siguiente. Esto le informa a la clase sobre nuestra nueva columna donde almacenaremos la dirección de correo electrónico de la cuenta.
1 |
[Table("UserProfile")] |
2 |
public class UserProfile |
3 |
{
|
4 |
[Key] |
5 |
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)] |
6 |
public int UserId { get; set; } |
7 |
public string UserName { get; set; } |
8 |
public string EmailAddress { get; set; } |
9 |
}
|
Abre el archivo AccountController.cs
. Después de la función RemoveExternalLogins
, agrega el siguiente código para crear una nueva acción. Esto obtendrá el correo electrónico actual del usuario que inició sesión y lo pasará a la vista de la acción.
1 |
public ActionResult ChangeEmail() |
2 |
{
|
3 |
// Get the logged in user
|
4 |
string username = WebSecurity.CurrentUserName; |
5 |
string currentEmail; |
6 |
|
7 |
using (UsersContext db = new UsersContext()) |
8 |
{
|
9 |
UserProfile user = db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == username); |
10 |
currentEmail = user.EmailAddress; |
11 |
}
|
12 |
|
13 |
return View(currentEmail); |
14 |
}
|
También necesitamos agregar la vista correspondiente para esta acción. Este debería ser un archivo llamado ChangeEmail.aspx
en la carpeta Views\Account
:
1 |
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<string>" %> |
2 |
|
3 |
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> |
4 |
Change Email Address |
5 |
</asp:Content>
|
6 |
|
7 |
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> |
8 |
|
9 |
<hr>
|
10 |
<h2>Change Email Address</h2> |
11 |
|
12 |
<p>Current Email Address: <%= Model ?? "<i>No Current Email</i>" %></p> |
13 |
|
14 |
<% using(Html.BeginForm()) { %> |
15 |
<input type="text" name="newemail" /> |
16 |
<input type="submit" value="Change Email" /> |
17 |
<% } %> |
18 |
|
19 |
</asp:Content>
|
20 |
|
21 |
<asp:Content ID="Content3" ContentPlaceHolderID="FeaturedContent" runat="server"> |
22 |
</asp:Content>
|
23 |
|
24 |
<asp:Content ID="Content4" ContentPlaceHolderID="ScriptsSection" runat="server"> |
25 |
</asp:Content>
|
Esto nos da una nueva página que podemos usar para cambiar la dirección de correo electrónico del usuario actualmente conectado.



Si ejecutamos esta página y vamos a la acción /Account/ChangeEmail
, ahora vemos que actualmente no tenemos un correo electrónico. Pero tenemos un cuadro de texto y un botón que podemos usar para corregir eso. Sin embargo, primero debemos crear la acción que se ejecutará cuando se envíe el formulario de esta página.
1 |
[HttpPost] |
2 |
public ActionResult ChangeEmail(ChangeEmailModel model) |
3 |
{
|
4 |
string username = WebSecurity.CurrentUserName; |
5 |
|
6 |
using (UsersContext db = new UsersContext()) |
7 |
{
|
8 |
UserProfile user = db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == username); |
9 |
user.EmailAddress = model.NewEmail; |
10 |
db.SaveChanges(); |
11 |
}
|
12 |
|
13 |
// And to verify change, get the email from the profile
|
14 |
ChangeEmailModel newModel = new ChangeEmailModel(); |
15 |
using (UsersContext db = new UsersContext()) |
16 |
{
|
17 |
UserProfile user = db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == username); |
18 |
newModel.CurrentEmail = user.EmailAddress; |
19 |
}
|
20 |
|
21 |
return View(newModel); |
22 |
}
|
Después de realizar este cambio, ejecuta el sitio web y nuevamente ve a la acción /Account/ChangeEmail
que acabamos de crear. Ahora puedes ingresar una nueva dirección de correo electrónico y hacer clic en el botón Cambiar correo electrónico y ver que la dirección de correo electrónico se actualizará.
Atacando el sitio
Tal como está escrita, nuestra aplicación es vulnerable a un ataque de falsificación de solicitud entre sitios. Agreguemos una página web para ver este ataque en acción. Vamos a agregar una página dentro del sitio web que cambiará el correo electrónico a un valor diferente. En el archivo HomeController.cs
agregaremos una nueva acción llamada AttackForm
.
1 |
public ActionResult AttackForm() |
2 |
{
|
3 |
return View(); |
4 |
}
|
También agregaremos una vista para este AttackForm.aspx
dentro de la carpeta /Views/Home
. Debe tener un aspecto como este:
1 |
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %> |
2 |
|
3 |
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> |
4 |
Attack Form |
5 |
</asp:Content>
|
6 |
|
7 |
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> |
8 |
|
9 |
<hr>
|
10 |
<h2>Attack Form</h2> |
11 |
|
12 |
<p>This page has a hidden form, to attack you, by changing your email:</p> |
13 |
|
14 |
<iframe width="1px" height="1px" style="display:none;"> |
15 |
<form name="attackform" method="POST" action="<%: Url.Action("ChangeEmail", "Account") %>"> |
16 |
<input type="hidden" name="NewEmail" value="newemail@evilsite.com"/> |
17 |
</form>
|
18 |
</iframe>
|
19 |
<script type="text/javascript"> |
20 |
document.attackform.submit(); |
21 |
</script>
|
22 |
|
23 |
</asp:Content>
|
24 |
|
25 |
<asp:Content ID="Content3" ContentPlaceHolderID="FeaturedContent" runat="server"> |
26 |
</asp:Content>
|
27 |
|
28 |
<asp:Content ID="Content4" ContentPlaceHolderID="ScriptsSection" runat="server"> |
29 |
</asp:Content>
|
Nuestra página anuncia amablemente sus malas intenciones, lo que, por supuesto, no funcionaría con un ataque real. Esta página contiene un formulario oculto que no será visible para el usuario. Luego usa Javascript para enviar automáticamente este formulario cuando se carga la página.



Si ejecutas el sitio nuevamente y vas a la página /Home/AttackForm
, verás que se carga bien, pero sin ninguna indicación externa de que haya sucedido algo. Sin embargo, si ahora vas a la página /Account/ChangeEmail
, verás que tu correo electrónico ha sido cambiado a newemail@evilsite.com
. Aquí, por supuesto, lo estamos haciendo obvio intencionalmente, pero en un ataque real, es posible que no notes que tu correo electrónico ha sido modificado.
Mitigación de la falsificación de solicitudes entre sitios
Hay dos formas principales de mitigar este tipo de ataque. Primero, podemos verificar la referencia de la que llega la solicitud web. Esto debería indicarle a la aplicación cuando el envío de un formulario no proviene de nuestro servidor. Sin embargo, esto tiene dos problemas. Muchos servidores proxy eliminan esta información de referencia, ya sea intencionalmente para proteger la privacidad o como un efecto secundario, lo que significa que una solicitud legítima no podría contener esta información. También es posible que un atacante falsifique la referencia, aunque aumenta la complejidad del ataque.
El método más eficaz es exigir que exista un token específico de usuario para cada envío de formulario. El valor de este token debe generarse aleatoriamente cada vez que se crea el formulario y el formulario solo se acepta si se incluye el token. Si falta el token o se incluye un valor diferente, no permitimos el envío del formulario. Este valor se puede almacenar en el estado de la sesión del usuario o en una cookie para permitirnos verificar el valor cuando se envía el formulario.
ASP.NET facilita este proceso, ya que el soporte CSRF está integrado. Para usarlo, solo necesitamos hacer dos cambios en nuestro sitio web.
Arreglando el problema
Primero, debemos agregar el token único al formulario para cambiar el correo electrónico del usuario cuando lo mostramos. Actualiza el formulario en la vista ChangeEmail.aspx
en /Account/ChangeForm
:
1 |
<% using(Html.BeginForm()) { %> |
2 |
<%: Html.AntiForgeryToken() %> |
3 |
<%: Html.TextBoxFor(t=>t.NewEmail) %> |
4 |
<input type="submit" value="Change Email" /> |
5 |
<% } %> |
Esta nueva línea: <%: Html.AntiForgeryToken() %>
le dice a ASP.NET que genere un token y lo coloque como un campo oculto en el formulario. Además, el framework se encarga de colocarlo en otra ubicación donde la aplicación pueda acceder a él posteriormente para verificarlo.
Si recargamos la página ahora y miramos la fuente, veremos esta nueva línea, en el formulario, renderizada en el navegador. Este es nuestro token:
1 |
<form action="/Account/ChangeEmail" method="post"><input name="__RequestVerificationToken" type="hidden" value="g_ya1gqEbgEa4LDDVo_GWdGB8ko0Y91p98GTdKVKUocEBy-xAoH_Pok4iMXMxzZWX_IDDAXEkVwu3gc6UNzRKt8tjZ88I9t4NE8WT0UTT0o1" /> |
2 |
<input id="NewEmail" name="NewEmail" type="text" value="" /> |
3 |
<input type="submit" value="Change Email" /> |
4 |
</form>
|
También necesitamos realizar un cambio en nuestra acción para informarle que hemos agregado este token y que debe verificar el token antes de aceptar el formulario publicado.
Nuevamente, esto es simple en ASP.NET MVC. En la parte superior de la acción que creamos para manejar el formulario publicado, el que tiene el atributo [HttpPost]
agregado, agregaremos otro atributo llamado [ValidateAntiForgeryToken]
. Esto hace que el inicio de nuestra acción ahora se vea así:
1 |
[HttpPost] |
2 |
[ValidateAntiForgeryToken] |
3 |
public ActionResult ChangeEmail(ChangeEmailModel model) |
4 |
{
|
5 |
string username = WebSecurity.CurrentUserName; |
6 |
*rest of function omitted* |
Probemos esto. Primero ve a la página /Account/ChangeEmail
y restaura el correo electrónico de tu cuenta a un valor conocido. Luego podemos regresar a la página /Home/AttackForm
y nuevamente el código de ataque intenta cambiar nuestro correo electrónico. Si regresas a la página /Account/ChangeEmail
nuevamente, esta vez verás que tu correo electrónico ingresado anteriormente aún está seguro e intacto. Los cambios que hicimos en nuestro formulario y acción han protegido a esta página del ataque.
Si tuvieras que mirar el formulario de ataque directamente (se hace fácilmente eliminando las etiquetas <iframe>
alrededor del formulario en la página de ataque, verás el error que realmente ocurre cuando el formulario de ataque intenta publicar.



Estas dos líneas adicionales agregadas al sitio fueron suficientes para protegernos de este error.
Conclusión
La falsificación de solicitudes entre sitios es uno de los ataques más comunes y peligrosos en sitios web. A menudo se combinan con otras técnicas que buscan debilidades en el sitio para facilitar la realización del ataque. Aquí he demostrado una forma de proteger tu sitio .NET contra este tipo de ataque y hacer que tu sitio web sea más seguro para tus usuarios.