Securizar, Autorizar, Validar

Creo firmemente en maximizar el valor de lo que tecleo y hacer pública la información que pueda ser útil a alguien (si me dejan, claro), por lo que en este post voy a intentar resumir una conversación reciente sobre securización, autorización y validación en aplicaciones.

Antes de nada, agradecer a Sergio León que me ha permitido usar la conversación y a Antonio Chamorro y el resto de compañeros de ambos por todo el análisis previo que hicieron del tema.

El escenario es el siguiente: tenemos una aplicación que recibe entradas del usuario y necesitamos asegurarnos de que la aplicación funciona correctamente. ¿Qué quiere decir correctamente? Pues que un usuario no puede hacer un uso ilegítimo de la aplicación (por ejemplo mediante técnicas de SQL-injection, XSS o similares), que un usuario no puede introducir datos inválidos, que un usuario no puede acceder a información a la que no está autorizado y que las acciones de un usuario no pueden corromper la información con que trabaja la aplicación (es decir, ésta mantiene sus invariantes internos).

Aunque a priori todas estas protecciones están relacionadas, cada una de ellas representa un tipo de problema diferente y, como tal, la forma de enfocarlo no es la misma.

Securización

El primer nivel del que nos debemos preocupar es la securización (palabra no recogida por la RAE, por cierto) de la aplicación, entendiendo como tal que un atacante no pueda usar la aplicación con fines ilegítimos, como por ejemplo borrar toda la base de datos o hacer que nuestra aplicación realice cargos en la cuenta de Paypal de un tercero.

Hoy en día casi todos los frameworks usados para desarrollar aplicaciones (ya sean aplicaciones web, de escritorio, APIs, etc.) incluyen herramientas para evitar los tipos de ataque más comunes. En este nivel encontraríamos técnicas como el uso de consultar SQL parametrizadas para evitar ataques de SQL-injection, el cifrado de las comunicaciones para impedir la captura de credenciales y el secuestro de sesión, el filtrado de datos introducidos para protegernos de ataques de cross site scripting (XSS), etc.

Aplicar estas técnicas suele ser relativamente fácil (aunque especialmente divertido) y no es algo que podamos elegir. Debemos aplicarlas sí o sí. La aplicación web inofensiva que hoy está desplegada en una intranet puede acabar en Azure o incluso puede ser atacada desde la propia intranet. Además, existe mucha documentación al respecto y no merece la pena saltarse las prácticas recomendadas y asumir riesgos innecesarios.

Autorización

La autorización consiste en asegurarnos de que un usuario no puede acceder a información o realizar operaciones para las que no está autorizado. A primera vista puede parecer que es más o menos lo mismo que la securización de la que hablábamos antes, pero la autorización suele referirse más a usuarios legítimos. Es decir, se trata de controlar que un comercial puede consultar la información de sus clientes pero no de los clientes de otro comercial, o que el jefe de compras puede cambiar el precio de coste de un producto pero no incrementar el riesgo concedido a un cliente.

Existen frameworks que tienen soporte, en mayor o menor medida, para resolver este problema de una forma declarativa. Es el caso de los filtros Authorize que encontramos en ASP.NET MVC. Por desgracia es díficil resolver esto de una forma completamente límpia y ortogonal al código de negocio y a veces no queda más remedio que mezclarlo, especialmente si se usa un interface de usuario «tradicional», con un formulario para editar un montón de campos relativos a una entidad. Con un interface de usuario más orientado a tareas, en el que no tuviera una pantalla de «Editar Producto», sino una pantalla de «Asignar precio de coste a producto», sería más sencillo conseguirlo, pero en cualquier caso, lo más probable es que te toque remangarte y meter código «ligeramente feo» en algunas partes de la aplicación para asegurarte de que se aplican correctamente los permisos.

Veréis que hasta ahora no he mencionado el tema de autenticación, que consiste en saber quién es el usuario que se conecta a la aplicación. Obviamente es una cuestión importante, porque si no tengo identificado al usuario, malamente podré saber qué está autorizado a hacer, pero hoy en día ese un problema bastante bien resuelto en cualquier plataforma por lo que no suele suscitar muchas dudas.

Validación y mantenimiento de invariantes

Aquí la cosa se pone más interesante. Supongamos que tenemos un usuario legítimo, que se ha autenticado en la aplicación y que está intentando realizar una operación para la que hemos comprobado que está autorizado. ¿Debemos confiar en él? Como ya te habrás imaginado la respuesta es, rotundamente no.

Cuando el usuario introduce información, debemos comprobar siempre la información que está introduciendo. El nivel más básico de comprobación sería validar que los tipos de datos se ajustan a lo que esperamos, es decir, que las fechas son fechas, los números son números, etc. Aquí no se trata tanto de protegernos del usuario (que también) sino de proporcionarle una experiencia de uso adecuada, ayudándole a corregir posibles errores en la información introducida.

Este primer tipo de validación, básicamente de formato y cosas similares, es fácil de realizar mediante las herramientas incorporadas en cualquier framework medio decente, o incluso con librerías propias. Generalmente se puede llegar a una solución bastante declarativa o, al menos, basada en reglas muy simples que se pueden reutilizar en distintos contextos. En aras de conseguir una buena experiencia de usuario, es importante realizar esta validación tan cerca del usuario y tan pronto como sea posible para evitar frustrarle cuando después de rellenar un formulario de 30 campos lo envíe al servidor y, 5 segundos después, reciba un mensaje diciéndole que el formato de la fecha es incorrecto.

Desgraciadamente, en la mayoría de aplicaciones esto no basta y hay cosas que no se pueden validar con una expresión regular y un [MaxLength("40")]. Por ejemplo, en una aplicación de tienda online, podría ser necesario validar que el usuario no ha seleccionado un producto que no puede comprar por restricciones de edad, de país de residencia o, sencillamente, porque el producto ha sido descatalogado.

Hay ocasiones en las que, ante un escenario similar, el desarrollador opta por no realizar ninguna validación basándose en que el usuario ha tenido que elegir el producto de entre los productos que le hemos mostrado en la página, y al cargar esos productos ya hemos validados que fuesen «comprables» por ese usuario, por lo que ya no hay nada de lo que preocuparse. ¿O sí?

En primer lugar, que al usuario le mostremos una lista de opciones no quiere decir que él nos conteste con una opción de esta lista. Podría modificar el POST HTTP para enviarnos un código de producto distinto y conseguir así saltarse alguna restricción. Otro problema potencial es que desde que enviamos la información de productos disponibles al usuario hasta que el usuario realiza la compra, puede pasar el tiempo suficiente como para que uno de esos productos deje de ser válido.

Estos problemas pueden ser más o menos graves dependiendo del tipo de negocio. En la mayoría de los casos no es tan grave que un pedido acabe guardándose con un producto incorrecto porque luego hay otras fases en las que se puede validar ese pedido y, si fuera necesario, rechazarlo. Aun así, en determinados escenarios puede ser crítico asegurar la validez del producto añadido al pedido, por ejemplo, para evitar que un menor pueda comprar un medicamento.

Normalmente este tipo de controles no debería dar problemas casi nunca. En el 99% de los casos, los datos deberían ser válidos y no haría falta notificar al usuario. Por ese motivo, no suelo darles el mismo tratamiento que a las típicas validaciones de las que hablaba antes y, habitualmente, los compruebo aplicando técnicas de Design By Contract en los componentes internos de la aplicación y haciendo que estos lancen excepciones si se produce alguna violación del contrato.

Si las comprobaciones a realizar son costosas, puede ser interesante plantearse el realizarlas offline, haciendo que los datos se almacenen como «no verificados» hasta que un proceso batch se encargue de comprobar su validez y, en caso de que haya algún problema, notificárselo al usuario mediante, por ejemplo, un correo electrónico.

En el caso de que estos problemas se puediese producir con una mayor frecuencia, encontes sí es recomendable pasar esa validación a un nivel más cercano al usuario y hacerla más amigable.

Conclusión

Todos los que leéis este blog sabéis que una de las cosas que más preocupa al desarrollar software es asegurar que tiene una calidad adecuada, y los factores que aparecen en este post son muy importantes para conseguirlo.

En un mundo ideal, toda aplicación debería estar sometida a auditorías de seguridad externas, validar todas las entradas y aplicar siempre las reglas de negocio que garantizan la integridad de los datos. Por desgracia, eso cuesta tiempo (y por tanto dinero) y no siempre es viable (o rentable).

Lo importante es ser consciente de lo que se está dejando de hacer, lo que se ahorra con ello, los riesgos que se asumen y el coste que tendrían. Sólo en base a eso se puede tomar una decisión adecuada de hasta dónde hay que llegar en estos temas.

4 comentarios en “Securizar, Autorizar, Validar

  1. Hola Juanma,

    Ya te lo agradecí al correo pero te vuelvo a agradecer ahora el esfuerzo realizado para ayudarnos con este tema.

    La verdad es que, después de nuestra conversación y del post que has escrito, todo está muy claro y la pregunta es ¿Por qué no he hecho antes Desing By Contract? Seguro que hay un montón de frameworks para ello. Me consta Code Contracts (que tengo la sensación está en desuso o simplemente no ha terminado de arrancar), está también Spec# http://research.microsoft.com/en-us/projects/specsharp/ que no tengo muy claro si sustituye, mejora o se inspira en Code Contracts, pero lo que es obvio es que, en el negocio, hay que validar toda la entrada y no confiar en nada ni en nadie.

    Yo por mi parte, estoy ahora apostando por una sencilla clase de precondiciones como la siguiente (dejo a un lado invariantes y postcondiciones porque aunque sé, son importantes, todavía no veo muy bien donde encajan en un proceso de validación de entrada de usuario «puro»).

    public static class DbC
    {
    public static void Require(Func condition, dynamic value, string message)
    {
    if (!condition(value))
    {
    throw new DbCException(message);
    }
    }
    }

    Lógicamente, iré metiendo métodos según lo necesite, pero si veo que se complica en exceso y no queriendo reinventar la rueda, ya miraré algún framework específico.

    Genial artículo!! :)

  2. Juan María Hernández dijo:

    Hola Sergio,

    Lo del DbC tiene dos vertientes, una sería la parte de «aserción», es decir, comprobar que algo se cumple en un punto determinado durante la ejecución del programa, y otra la de análisis estático, para validar en tiempo de compilación si el programa se comporta correctamente (esto tiene ciertas limitaciones que no vienen al caso, pero aquí es donde entrarían cosas como Spec# o algunas herramientas que existen en lenguajes como Eiffel).

    Para el caso de validar con aserciones, no creo que haga falta complicarse con un framework ni nada parecido. Con una clase como la que tienes y un par de métodos de ayuda más, tienes más que suficiente.

    ¡Gracias por tu comentario!

  3. Juan María Hernández dijo:

    Hola Luis,

    La verdad es que le tenía perdida la pista, pero parece que Code Contracts sigue vivo viendo ese documento.

    De todas formas, para usar DbC y como le decía a Sergio, yo partiría más de las bases teóricas (http://en.wikipedia.org/wiki/Design_by_contract) e incluso miraría un poco la manera en que se enfoca en el mundo de Eiffel (http://www.eiffel.com/developers/design_by_contract.html) más que tratar de buscar una librería/implementación concreta.

    Muchas gracias por el enlace.

Comentarios cerrados.