Manteniendo el Estado en Nuestras Aplicaciones Web

Una grandísima proporción de los sistemas desarrollados hoy en día siguen el paradigma cliente-servidor. Y si bien hay diferentes maneras de implementarlo, indudablemente la más utilizada actualmente 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 (markup) suficientemente simple, pero también 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 distribuidas.

El estándar del protocolo HTTP define cuatro “verbos”, que a su vez definen distintos tipos de solicitudes:

• GET: solicitud de información sin requerir cambio de estado.
• POST: interacción por medio de la cual el cliente manda información compleja, que determinará la naturaleza de la respuesta.
• PUT: creación de un nuevo objeto en el servidor.
• DELETE: destrucción de un determinado objeto en el servidor.

En la práctica, la mayoría de los sistemas web hace caso omiso de estos verbos, ignorando a través de qué verbo llegó una solicitud determinada. Incluso varios navegadores ni siquiera implementan
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 riesgo: deja en el programador la responsabilidad de implementar cómo manejar la interacción repetida sobre un protocolo que delega el mantener el estado o “sesión” a una capa superior. HTTP fue concebido como un protocolo a través del cual se solicitaría información estática, por lo que para un servidor HTTP toda solicitud es única. A esto nos referimos cuando decimos que es un protocolo sin estado (stateless).

En los sistemas web más primitivos, la forma de simular “sesiones” es incluyendo en cada solicitud toda la información del estado. Dichos sistemas hacen un uso extensivo de los campos ocultos (hidden) en todos los formularios y ligas internas, transportando en cada solicitud información tal como: quién es el usuario en cuestión, en qué paso de una transacción se encuentra, preferencias que ha manifestado a lo largo de su interacción.

Sin embargo, dicho mecanismo resulta no sólo engorroso, sino también muy frágil: un usuario malicioso o curioso (llamémosle “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 de 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. El uso de las galletas libera al desarrollador del lío 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 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 difícil 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, mejor guardamos una cadena encriptada que apunte a un registro en nuestra base de datos. En este mismo sentido, tampoco debemos grabar directamente el ID de la sesión (siendo sencillamente un número, sería trivial para un atacante 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), que 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 en la actualidad, son las variaciones del algoritmo SHA desarrollado por el NIST y publicado en 1994. Usar las bibliotecas que los implementan es verdaderamente común.

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, ya que un atacante puede fácilmente crear un diccionario del resultado de aplicar SHA1 a la conversión de los diferentes números en cadenas. 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.

Consideraciones
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 inoperante. Retomando el asunto de las acciones invocadas a partir de recibir una petición tipo GET, un criterio de diseño debe ser que toda solicitud GET sea “idempotente”. Es decir que un GET no debe alterar de manera significativa
el estado de los datos. Un GET puede por ejemplo: aumentar el contador de visitas, pero no generar cambio substantivos a los datos. Un ejemplo de algo que no se debe hacer sería permitir que por medio
de un GET se pudiera eliminar cierto objeto. Si hacemos esto, corremos el riesgo de que dicha acción sea disparada de forma inesperada e indiscriminada por robots indexadores de buscadores como Google. Es por ello que toda acción que genere un cambio en el estado sustantivo de nuestra base de datos debe llevarse a cabo a través de una solicitud tipo POST.

Referencias
[1]. Descripción del protocolo HTTP implementado en 1991. www.w3.org/Protocols/HTTP/AsImplemented.html
[2]. Mecanismo de manejo de estado sobre HTTP, RFC 2965. www.ietf.org/rfc/rfc2965.txt
[3]. Funciones criptográficas de resumen. en.wikipedia.org/wiki/Cryptographic_hash_ function
[4]. Funciones de resumen SHA. en.wikipedia.org/wiki/SHA_hash_functions

Acerca del Autor

Gunnar Wolf es administrador de sistemas para el Instituto de Investigaciones Económicas de la UNAM; entusiasta y promotor del Software Libre, desarrollador del proyecto Debian GNU/Linux desde el 2003, miembro externo del Departamento de Seguridad en Cómputo de DGSCA-UNAM desde 1999