martes, 5 de noviembre de 2013

Cross Site Request Forgery y $.ajax en ASP.NET MVC

En la aplicación web en la que estamos trabajando actualmente ha habido un pequeño brote de histeria colectiva porque, de repente y sin previo aviso, nos hemos percatado que no éramos todo lo decorosos que debíamos ser con la seguridad.

Siendo así, hemos verificado que ya estamos protegidos contra con los ataques conocidos más populares… a excepción del Cross Site Request Forgery. La verdad es que si trabajas con ASP.NET MVC es muy sencillo protegerse de este ataque porque el propio framework ya incorpora de serie una serie de clases y helpers que te ayudan a estar seguro.

En este post se explica muy bien el concepto y como se implementa en ASP.NET MVC.

Sin embargo, si utilizas llamadas Ajax la cosa se complica un poco. Hay soluciones variopintas pero casi todas buscan lo mismo: anexar automáticamente el parámetro __RequestVerificationToken a la petición enviada. Esto se complica si la petición es JSON porque no hay donde anexar el parámetro, bueno sí, a la url, pero queda feo y hace mucho ruido.

Al final, cogiendo de aquí y de allí, hemos configurado nuestro propio atributo AntiForgeryToken que funciona como nosotros queremos y pensamos cumple con casi todos los escenarios.

El gran problema (a mi entender) del atributo ValidateAntiForgeryToken es que busca el parámetro __RequestVerificationToken sólo en la colección Form. Nuestra solución ha sido simplemente no buscar sólo en Form, sino también en QueryString y en Headers.

Además (y tomada prestada la idea de aquí, gracias luisxkimo), también hemos hecho que el nuevo atributo se limite por defecto a los verbos POST y también se puede utilizar a nivel de clase.

Por otro lado, también hay un pequeño código javascript necesario (y seguro mejorable) que ayuda a que las peticiones vía ajax incorporen automáticamente la cabecera adecuada para superar la validación.

El código de servidor es el siguiente:

using System;

using System.Web.Helpers;

using System.Web.Mvc;

 

namespace WebApplication1

{

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]

    public class MyValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter

    {

        private readonly AcceptVerbsAttribute _acceptVerbs;

 

        public MyValidateAntiForgeryTokenAttribute()

            : this(HttpVerbs.Post)

        {

 

        }

 

        public MyValidateAntiForgeryTokenAttribute(HttpVerbs verbs)

        {

            _acceptVerbs = new AcceptVerbsAttribute(verbs);

        }

 

        public void OnAuthorization(AuthorizationContext filterContext)

        {

            var request = filterContext.RequestContext.HttpContext.Request;

            var requestType = request.RequestType;

            if (!_acceptVerbs.Verbs.Contains(requestType))

            {

                return;

            }           

            var cookie = request.Cookies[AntiForgeryConfig.CookieName];

            var cookieToken = cookie != null ? cookie.Value : "";

            var name = "__RequestVerificationToken";

            var formToken = request.Form[name];

            if (string.IsNullOrWhiteSpace(formToken))

            {

                formToken = request.Headers[name];

            }

            if (string.IsNullOrWhiteSpace(formToken))

            {

                formToken = request.QueryString[name];

            }

            AntiForgery.Validate(cookieToken, formToken);

        }

    }

}

El código de cliente busca primero si hay un campo __RequestVerificationToken disponible y además comprueba el tipo de petición:

 

        $(document).ajaxSend(function (event, jqXHR, ajaxSettings) {

            var type = ajaxSettings.type.toUpperCase();

            if (["POST"].indexOf(type) != -1) {

                var $token = $("[name='__RequestVerificationToken']");

                if ($token.length > 0) {

                    var token = $token.first().val();

                    jqXHR.setRequestHeader("__RequestVerificationToken", token);

                }

            }

        });

Ahora toca ponerlo en producción!

Un saludo!

1 comentario:

  1. No se si te servirá, pero yo he encontrado una forma sencilla de mantener el sistema de MVC en las peticiones por AJAX.

    Te copio un trozo de código, para que veas el concepto.

    var lT = $('< form >@Html.AntiForgeryToken()< / form >').serialize();
    lT += "&Id=" + lItem.Id + "&LoQueSea=" + lsMasDatos;
    $.ajax({
    type: "POST",
    url: lURLPeticion,
    data: lT
    })

    De esta forma, toda petición AJAX que modifique algo, se realiza por POST, y lo único que debes hacer es serializar el Html.AntiForgeryToken() dentro de las etiquetas de un FORM, y mandarlo como datos de la petición AJAX.

    En el servidor no tienes que cambiar nada, decorar el método para que pida el TOKEN y que solo responda por POST.

    No se si esto te servirá, le veras limitaciones, o no lo veas correcto, estoy abierto a dialogar sobre el tema, los temas sobre seguridad son muy importantes, y compartiendo conocimiento se aprende mucho mas.

    ResponderEliminar