Search

Search this site:

Manteniendo el estado en nuestras aplicaciones Web: Una historia de galletas y de resúmenes

Una grandísima proporción de los sistemas desarrollados hoy en día, siguen el paradigma cliente-servidor. Y si bien hay muy diferentes maneras de implementar sistemas cliente-servidor, indudablemente la más difundida hoy por hoy es la de los sistemas Web.

La conjunción de un protocolo verdaderamente simple para la distribución de contenido (HTTP) con un esquema de marcado suficientemente simple pero suficientemente rico para presentar una interfaz de usuario con la mayor parte de las funciones requeridas por los usuarios (HTML) crearon el entorno ideal para el despliegue de aplicaciones distribuídas.

Desde sus principios, el estándar de HTTP menciona cuatro verbos por medio de los cuales se puede acceder a la información: GET (solicitud de información sin requerir cambio de estado), POST (interacción por medio de la cual el cliente manda información compleja y que determinará la naturaleza de la respuesta), PUT (creación de un nuevo objeto en el servidor) y DELETE (destrucción de un determinado objeto en el servidor). Sin embargo, por muchos años, éstos fueron mayormente ignorados — La mayor parte de los sistemas hace caso omiso a través de qué verbo llegó una solicitud determinada; muchos navegadores no implementan siquiera PUT y DELETE, dado su bajísimo nivel de uso — Aunque con la popularización del paradigma REST, esto probablemente esté por cambiar.

El protocolo HTTP, sin embargo, junto con su gran simplicidad aportó un gran peligro — No una vulnerabilidad inherente a los sistemas Web, sino que un peligro derivado de que muchos programadores no presten atención a un aspecto fundamental de los sistemas Web: Cómo manejar la interacción repetida sobre de un protocolo que delega el mantener el estado o sesión a una capa superior. Esto es, para un servidor HTTP, toda solicitud es única. En especial, un criterio de diseño debe ser que toda solicitud GET sea idempotente — Esto significa que un GET no debe alterar de manera significativa[fn]Es aceptable que a través de un GET, por ejemplo, aumente el contador de visitas, esto es, que haya un cambio no substantivo — Pero muchos desarrolladores han sufrido por enlazar a través de un GET (generado por una liga HTML estándar), por ejemplo, el botón para eliminar cierto objeto. Toda acción que genere un cambio en el estado substantivo de nuestra base debe llevarse a cabo a través de POST. ¿Qué pasa en caso contrario? Que diversas aplicaciones, desde los robots indexadores de buscadores como Google y hasta aceleradores de descargas ingenuos que buscan hacer más ágil la navegación de un usuario (siguiendo de modo preventivo todas las ligas GET de nuestro sistema para que el usuario no tenga que esperar en el momento de seleccionar alguna de las acciones en la página) van a disparar éstos eventos de manera inesperada e indiscriminada.[/fn] el estado de los datos.

HTTP fue concebido [1] como un protocolo a través del cual se solicitaría información estática. Al implementar las primeras aplicaciones sobre HTTP[fn]En términos de redes, HTTP implementa exclusivamente la capa 4 del modelo OSI, y si bien TCP mantiene varios rasgos que nos permiten hablar tambien de sesiones a nivel conexión, estas son sencillamente descartadas. Las capas 5 y superiores deben ser implementadas a nivel aplicación.[/fn], nos topamos con que cada solicitud debía incluir la totalidad del estado. Es por esto que los muchos sistemas Web hacen un uso extensivo de los campos ocultos (hidden) en todos sus formularios y ligas internas, transportando los valores de interacciones previas que forman parte conceptualmente de una sóla interacción distribuída a lo largo de varios formularios, o números mágicos que permiten al servidor recordar desde quién es el usuario en cuestión hasta todo tipo de preferencias que ha manifestado a lo largo de su interacción.

Sin embargo, éste mecanismo resulta no sólo muy engorroso, sino que muy frágil: Un usuario malicioso o curioso (llamémosle genéricamente «atacante») puede verse tentado a modificar estos valores; es fácil capturar y alterar los campos de una solicitud HTTP a través de herramientas muy útiles para la depuración. E incluso sin estas herramientas, el protocolo HTTP es muy simple, y puede “codificarse” a mano, sin más armas que un telnet abierto al puerto donde escucha nuestro sistema. Cada uno de los campos y sus valores se indican en texto plano, y modificar el campo «user_id» es tan fácil como decirlo.

En 1994, Netscape introdujo un mecanismo denominado galletas (cookies) que permite al sistema almacenar valores arbitrarios en el cliente[fn]La galleta será enviada en cada solicitud que se haga, por lo que se recomienda mantenerla corta. Varias implementaciones no soportan más de 4KB.[/fn]. Un año más tarde, Microsoft lo incluye en su Internet Explorer; el mecanismo fue estandarizado en 1997 y extendido en el 2000 con los RFCs 2109 y 2965 [2]. El uso de las galletas libera al desarrollador del engorro antes mencionado, y le permite implementar fácilmente un esquema verdadero de manejo de sesiones — Pero, ante programadores poco cuidadosos, abre muchas nuevas maneras de —adivinaron— cometer errores.

Dentro del cliente (típicamente un navegador) las galletas están guardadas bajo una estructura de doble diccionario — En primer término, toda galleta pertenece a un determinado servidor (esto es, al servidor que la envió). La mayor parte de los usuarios tienen configurados a sus navegadores, por privacidad y por seguridad, para entregar el valor de una galleta únicamente a su dominio origen (de modo que al entrar a un determinado sitio hostil éste no pueda robar nuestra sesión en el banco); sin embargo, nuestros sistemas pueden solicitar galletas arbitrarias guardadas en el cliente. Para cada servidor, podemos guardar varias galletas, cada una con una diferente llave, un nombre que la identifica dentro del espacio del servidor. Además de estos datos, cada galleta guarda la ruta a la que ésta pertenece, si requiere seguridad en la conexión (permitiendo sólo su envío a través de conexiones cifradas), y su periodo de validez, pasado el cual serán consideradas “rancias” y ya no se enviarán. El periodo de validez se mide según el reloj del cliente.

Guardar la información de estado del lado del cliente es riesgoso, especialmente si es sobre un protocolo tan simple como HTTP. No es dificil para un atacante modificar la información que enviaremos al servidor, y si bien en un principio los desarrolladores guardaban en las galletas la información de formas parciales, llegamos a una regla de oro: Nunca guardar información real en ellas. En vez, guardemos algo que apunte a la información. Esto es, por ejemplo, en vez de guardar el ID de nuestro usuario, una cadena criptográficamente fuerte [3] que apunte a un registro en nuestra base de datos. ¿A qué me refiero con esto? A que tampoco grabe directamente el ID de la sesión (dado que siendo sencillamente un número, sería para un atacante trivial probar con diferentes valores hasta “aterrizar” en una sesión interesante), sino una cadena aparentemente aleatoria, creada con un algoritmo que garantice una muy baja posibilidad de colisión y un espacio de búsqueda demasiado grande como para que un atacante lo encuentre a través de la fuerza bruta.

Los algoritmos más comunes para este tipo de uso son los llamados funciones de resumen (digest) [3]. Estos generan una cadena de longitud fija; dependiendo del algoritmo hoy en día van de los 128 a los 512 bits. Las funciones de resumen más comunes hoy en día son las variaciones del algoritmo SHA desarrollado por el NIST y publicado en 1994; usar las bibliotecas que los implementan es verdaderamente trivial. Por ejemplo, usando Perl:

use Digest::SHA1; print Digest::SHA1->sha1_hex("Esta es mi llave");

nos entrega la cadena:

c3b6603b8f841444bca1740b4ffc585aef7bc5fa

Pero, ¿qué valor usar para enviar como llave? Definitivamente no queremos enviar, por ejemplo, el ID de la sesión - Esto nos pondría en una situación igual de riesgosa que incluir el ID del usuario. Un atacante puede fácilmente crear un diccionario del resultado de aplicar SHA1 a la conversión de los diferentes números en cadenas. La representacíon hexadecimal del SHA1 de ‘1’ siempre será d688d9b3d3ba401b25095389262a3ecd2ad5ad68, y del de 100 siempre será daaaa8121aa28fca0edb4b3e1f7b7c23d6152eed. El identificador de nuestra sesión debe contener elementos que varíen según algún dato no adivinable por el atacante (como la hora exacta del día, con precisión a centésimas de segundo) o, mejor aún, con datos aleatorios.

Este mecanismo nos lleva a asociar una cadena suficientemente aleatoria como para que asumamos que las sesiones de nuestros usuarios no serán fácilmente “secuestradas” (esto es, que un atacante no le atinará al ID de la sesión de otro usuario), permitiéndonos dormir tranquilos sabiendo que el sistema de manejo de sesiones en nuestro sistema es prácticamente inmune al ataque por fuerza bruta.

Como último punto: Recuerden que algunas personas, por consideraciones de privacidad, han elegido desactivar el uso de galletas en su navegación diaria, a excepción de los sitios que expresamente autoricen. Tomen en cuenta que una galleta puede no haber sido guardada en el navegador cliente, y esto desembocará en una experiencia de navegación interrumpida y errática para dichos usuarios. Es importante detectar si, en el momento de establecer una galleta, ésta no fue aceptada, para dar la información pertinente al usuario, para que sepa qué hacer y no se encuentre frente a un sistema inoperativo más.

</b>REFERENCIAS</b>

Attachments

Versión impresa en SG (primer página) (614 KB)

Versión impresa en SG (segunda página) (545 KB)