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!

sábado, 2 de noviembre de 2013

Intríngulis de los bundles de ASP.NET MVC

Una de las características que mas me gusta de ASP.NET MVC es su capacidad de crear bundles de javascript y css. Lo encuentro muy útil y además es una buena práctica que ninguna aplicación web debería pasar por alto.

En este post no quiero mostrar como crearlos, para eso ya existe un excelente tutorial en el portal de asp.net, Bundling and Minification, además de artículos sobre temas concretos que no cubre el anterior tutorial, como el fallback si utilizas CDN

Sin embargo, después de un tiempo utilizándolos, me he dado cuenta que tienen algunos comportamientos, cuando menos peculiares, que quizás requieran una explicación. En concreto, me voy a centrar en 2 ideas:

  • Dar soporte a los ficheros map
  • El oculto significado del parámetro virtualPath

Dar soporte a los ficheros map

Una de las grandes ventajas de utilizar bundles en que nuestro código javascript será minificado (o minimizado, nunca me siento a gusto con esta palabra). Siendo así, el tamaño del fichero se verá reducido… y también su contenido se verá alterado. Es decir, la minificación de los bundles de ASP.NET MVC, además de eliminar retornos de carro, comentarios, espacios, la palabra debugger, etc. también “ofusca” el código (aquí no me quiero meter en ningún jardín, pero yo creo que renombrar el nombre de la variable “saludo” al nombre “n” es ofuscar… digo yo) y por sí fuera poco también durante la minificación se puede tomar ciertas libertades y “optimizar” algunos bloques de código. Cuidado con esto último porque no sería la primera vez que la versión minificada no funciona correctamente porque al señor minificador se le fue la mano!

En cualquier caso, está claro que nuestro fichero .js original no es el mismo fichero .js que es servido al cliente. De este modo, cuando queramos depurar en cliente nuestro código javascript, no va a ser una tarea fácil. Pues bien, los ficheros .map vienen al rescate (un montón de info relacionada con la especificación la puedes encontrar aquí).

Si tenemos instalada la extensión Web Essentials en Visual Studio (y asumo que casi cualquier programador web la tendrá instalada), es muy fácil minificar un fichero. Simplemente click derecho y “Minify JavasScript file(s)”. Si nos fijamos, además de crear nuestro fichero .min.js, también creará un fichero .min.js.map

Por ejemplo, para el siguiente código en el fichero main.js

function holaMundo() {

    // Saludar

    var saludo = "Hola mundo!";

    alert(saludo);

}

Generará un fichero main.min.js y un fichero main.min.js.map

El fichero main.min.js

function holaMundo(){alert("Hola mundo!")}

//# sourceMappingURL=main.min.js.map

y el fichero main.min.js.map

{

"version":3,

"file":"main.min.js",

"lineCount":1,

"mappings":"AAAAA,SAASA,SAAS,CAAA,CAAG,CAMjBC,KAAK,CAFQ,aAER,CANY",

"sources":["main.js"],

"names":["holaMundo","alert"]

}

El caso es que el fichero .map nos servirá para poder “desminificar/desofucar” nuestro fichero .min.js en cliente.

Yo esto sólo lo he probado con Chrome pero entiendo que tarde o temprano lo soportarán todos los navegadores. Con Chrome, si pedimos la página sólo descargará el fichero .min.js (el que hemos cargado con la etiqueta <script>). Sin embargo, si tenemos previamente abiertas las Dev Tools (ojo que no vale abrirlas una vez ya estamos en la página), veremos que además de descargarse el fichero .min.js también se descargará el fichero .min.js.map (sólo si tenemos abiertas las Dev Tools, claro, sino sería un derroche).

image

image

Será cuando seleccionemos el fichero original (el no minificado) en la pestaña Sources cuando se descargará realmente el fichero main.js (el original) para poder comenzar a depurar o ver su contenido ¡mola!

image

Chrome por defecto carga los ficheros .map, esto se puede configurar desde las opciones del navegador.

image

Respecto a las opciones de Chrome, sólo mencionar que también existe el concepto de fichero .map para css. Por ejemplo Bootstrap en sus últimas versiones ya lo incorpora y no te asustes si ves peticiones a ficheros .less que “no” tienes en tu proyecto, porque son justamente parte del fichero .map y de la clave sourcesContent.

image

Hasta aquí todo genial… pero hasta aquí nada hemos hablado todavía de los bundles de ASP.NET MVC. Por ahora, sólo hemos utilizado Web Essentials y hemos servidor directamente el fichero .min.js.

Cuando metemos a los bundles en la ecuación… pues todo este escenario se va “tristemente” al traste. Esto es porque los bundles, lejos de detectar que ya existe un fichero .min.js y dejarlo intacto, lo detectan y ya puestos le meten otra “pasadita” al minificador, con lo que el comentario de Web Essentials que da soporte a los ficheros .map desaparece :( Es decir, nuestro anterior fichero .min.js pasa a tener el siguiente código:

function holaMundo(){alert("Hola mundo!")}

Siendo así, bundles y ficheros .map parecen un amor imposible.

¿La solución? Pues quizás hacerse un minificador personalizado (implementando IBundleTransform, eso en otro post…) o pasar de los bundles y utilizar sólo Web Essentials (que por cierto también da soporte a bundles, aunque de forma manual). Un ejemplo de bundles con Web Essentials se puede encontrar en este post Minify resources with source map at runtime using Web Essential

El oculto significado del parámetro virtualPath

El parámetro virtualPath es ese pequeño y aparentemente insignificante valor que se le pasa al constructor de ScriptBundle y StyleBundle. ¡Pon lo que quieras, dicen! ¡Es sólo un nombre!… pues no, amigos, ¡es más que un nombre! :)

Ya sabes que por defecto una aplicación ASP.NET MVC te crea algunos bundles, pues vamos a trabajar con el que carga los estilos del sitio (site.css) para ver primero el error y explicar después el porqué.

Por defecto, trae lo siguiente:

bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));

El único cambio que vamos a hacer es cambiar el valor del parámetro virtualPath a ~/Content (sin /css).

bundles.Add(new StyleBundle("~/Content").Include("~/Content/site.css"));

Y al pedir la página (siempre que estemos en release o con BundleTable.EnableOptimizations = true;… vamos, que se estén haciendo los bundles), tenemos un bonito error 403 Forbidden

image

Como no sabía por donde pillar esto, me he bajado la trial de Reflector (¡pedazo de software!) y he depurado la clase BundleHandler del ensamblado System.Web.Optimization. En concreto, en el método RemapHandlerForBundleRequests está escrito el siguiente código:

if (!virtualPathProvider.FileExists(appRelativeCurrentExecutionFilePath)

    && !virtualPathProvider.DirectoryExists(appRelativeCurrentExecutionFilePath))

{

    //devolver bundle

    return true;

}

return false;

Es decir, si virtualPath existe en disco NO se devolverá ningún bundle. En nuestro caso la carpeta ~/Content existe, así que !adiós bundle!

Con ScriptBundle no suele haber problemas porque por imitación al código de la plantilla del proyecto, llamamos a los bundles siempre “~/bundles/<algo>” (y la carpeta bundles no existe). Sin embargo con StyleBundle (y también por imitación) lo llamamos ~/Content/<algo>… y aquí sí suele haber problemas.

A mí esto me ha dado más un dolor de cabeza, sobre todo porque en desarrollo no pasa (no se están haciendo bundles) y luego en producción o durante las pruebas es cuando te salta la liebre.

Otro problema que nos puede acarrear virtualPath es que las rutas a imágenes de las hojas de estilo serán relativas al nombre seleccionado. Esto no es un problema en desarrollo (puesto que el bundle no se realiza) pero sí en producción.

Dicho esto, me siguen gustando los bundles, pero sabiendo lidiar con estos pequeños intríngulis.

Un saludo!

jueves, 17 de octubre de 2013

Binding de números y fechas “culture-sensitive” en ASP.NET MVC

Ahora que estoy metido de lleno en un desarrollo con ASP.NET MVC 4, pensé que sería buena idea leer un buen libro sobre el mismo. Mi elección fue Pro ASP.NET MVC 4. El libró me encantó y lo recomiendo.

En el capítulo “Model Binding” (obligado título para un capítulo de un libro que hable sobre ASP.NET MVC) me chocó mucho el siguiente texto (que cito literalmente)

“The DefaultModelBinder class use different culture settings to perform type conversions from different areas of the request data. The values that are obtained from URLs (the routing and query string data) are converted using culture-insensitive parsing, but values obtained from form data are converted taking cultureinto account.

The most common problem that this causes relates to DateTime values. Culture-insensitive dates are expected to be in the universal format yyyy-mm-dd. Form date values are expected to be in the format specified by the server. This means that a server set to the UK culture will expected dates to be in the form dd-mm-yyyy, whereas a server set to the US culture will expect the format mm-dd-yyyy, though in either case yyyy-mm-dd is acceptable too.

A date value won’t be converted if it isn’t in the right format. This means that we must make sure that all dates included in the URL are expressed in the universal format. We must also be careful when processing date values that users provide—the default binder assumes that the user will express dates in using the format of the server culture, something that unlikely to always happen in an MVC application that has international users.”

Como por suerte o por desgracia, todas las aplicaciones que desarrollamos en la empresa tiene que soportan internacionalización, el tema de formato de fechas y números es un tema que me preocupa especialmente.

El párrafo citado anterior viene a decir lo siguiente:

  • Los datos provenientes de la url (querystring y route values) son culture-insensitive. Es decir, esperan un formato determinado con independencia de la cultura establecida en el servidor.
  • Los datos provenientes del form son culture-sensitives. Esto es que sí tienen en cuenta la cultura del servidor.

Vamos a hacer unos pruebas para confirmar esto y verlo en directo. La cultura del servidor será es-ES (haciendo patria) y simplemente crearemos un acción como la siguiente:

public ActionResult Index(decimal? numero, DateTime? fecha)
{
    return View();
}

Ahora y con la ayuda de ScratchPad probaremos el binding de fechas y números.

POST http://localhost:6319/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 31
Host: localhost:6319

Numero=1,5&Fecha=31%2F12%2F2013

Todo perfecto, tanto 1,5 como 31/12/2013 son formatos válidos para es-ES.

Numero=1.5&Fecha=13%2F31%2F2013

Ninguna de los 2 parámetros cumple con el formato esperado, error.

Numero=1,5&Fecha=2013%2F12%2F31

Sorprendentemente, aunque form sea culture-sensitive, también acepta el formato yyyy-mm-dd para las fechas.

GET http://localhost:6319/?numero=1,5&fecha=31-12-2013 HTTP/1.1
Host: localhost:6319

Ninguna de los 2 parámetros cumple con el formato esperado, error.

GET http://localhost:6319/?numero=1.5&fecha=2013-12-31 HTTP/1.1
Host: localhost:6319

Ningún problema, especial atención el formato de la fecha: yyyy-mm-dd

GET http://localhost:6319/?numero=1.5&fecha=12-31-2013 HTTP/1.1
Host: localhost:6319

Otra sorpresa, aunque se suponía que el formato de fecha para url tenía que ser yyyy-mm-dd, aparentemente también acepta mm-dd-yyyy

Con esta situación está claro que bindear números y fechas y soportan internacionalización en tu aplicación se va a convertir en una pesadilla si utilizamos sólo y exclusivamente DefaultModelBinder. Es por ello, que resulta necesario crear unos binders personalizados para los tipos Decimal y DateTime.

El código es el siguiente:

using System;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;

namespace WebApplication1
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value
= base.BindModel(controllerContext, bindingContext);
if (value != null)
{
return value;
}
if (controllerContext.HttpContext.Request.HttpMethod == "POST")
{
return null;
}
var valueProviderResult
= bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null)
{
return null;
}
if (string.IsNullOrWhiteSpace(valueProviderResult.AttemptedValue))
{
return null;
}
var errors
= bindingContext.ModelState[bindingContext.ModelName].Errors;
if (errors.Any())
{
errors.Clear();
}
try
{
return (DateTime)valueProviderResult.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
return null;
}
}

public class DecimalModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value
= base.BindModel(controllerContext, bindingContext);
if (value != null)
{
return value;
}
if (controllerContext.HttpContext.Request.HttpMethod == "POST")
{
return null;
}
var valueProviderResult
= bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null)
{
return null;
}
if (string.IsNullOrWhiteSpace(valueProviderResult.AttemptedValue))
{
return null;
}
var errors
= bindingContext.ModelState[bindingContext.ModelName].Errors;
if (errors.Any())
{
errors.Clear();
}
try
{
return (Decimal)valueProviderResult.ConvertTo(typeof(Decimal), CultureInfo.CurrentCulture);
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
return null;
}
}
}
}
Para hacerlo funcionar en MVC, también es necesario registrar nuestros nuevos y flamantes binders desde el global.asax:
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder());
ModelBinders.Binders.Add(typeof(Decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(Decimal?), new DecimalModelBinder());

Con esto, además del comportamiento predeterminado del DefaultModelBinder, ahora tanto url (querystring y route values) como form serán culture-sensitive.



Un saludo!