Prácticas seguras de programación para sistemas Web

Safe practice of programming systems for web

RESUMEN

El campo de la seguridad en cómputo muchas veces es visto como uno donde el trabajo es investigativo (búsqueda de nuevas categorías de fallos), reactivo (corrección de fallos encontrados, o incluso buscarlos proactivamente) o, en el peor de los casos, un campo donde los personajes más destacados son quienes saben abusar de los sistemas ajenos. Nada más lejano de la realidad — En la seguridad en cómputo, el rol más importante debe ser el desarrollador de sistemas, una de las piezas más importantes de la sociedad actual.

Este trabajo parte definiendo qué debemos entender por seguridad en cómputo, y por qué este concepto debe ir más allá de definiciones duras y frías, para reflejar que antes que otra cosa, el cómputo es una disciplina con la misma flexibilidad que los estudios humanísticos.

La parte medular del trabajo se centra en ejemplificar, analizando tres categorías de vulnerabilidad informática de alto perfil hoy en día (inyecciones de SQL, Cross-Site Scripting y manejo de sesiones a través de galletas HTTP), de especial relevancia para los sistemas Web desarrollados hoy en día — Y poco abordadas en específico por los principales textos con que se enseña la disciplina.

PALABRAS CLAVE

Seguridad en cómputo, redefinición, inyecciones SQL, mapeo objeto-relacional, cross-site scripting, XSS, sesiones HTTP, galletas HTTP, resumen criptográfico

ABSTRACT

The Computer Security field is often seen as one where work is mostly devoted to research (finding new bug/vulnerability categories), reactive (fixing bugs as they are found, or even proactively looking for them), or in the worst case, a field where the most renowned characters are those who can abuse other systems. Nothing farther from truth — In Computer Security, the most important role must always be that of the systems developer, one of the key parts of today's society.

This work starts by defining what we want to understand about Computer Security, and why this concept must reach beyond the hard and cold definitions, to reflect that, before anything else, computing is a discipline with the same amount of flexibility than humanistic studies.

The core of this work is centered on exemplifying, by analizing three currently high-profile computer vulnerability categories which are specially relevant for Web systems developed today (SQL injections, cross-site scripting and session handling through HTTP cookies), which do not receive enough coverage by the main texts with which this discipline is learnt.

KEY WORDS

Computer Security, redefinition, SQL injection, object-relation mapper, cross-site scripting, XSS, HTTP sessions, HTTP cookies, cryptographic digest

Introducción

La Evolución del rol que cumplen los sistemas en las organizaciones ha cambiado por completo -afortunadamente- el punto de vista que la mayor parte de los desarrolladores tiene con respecto a la seguridad.

Hace una o dos décadas, el tema de la seguridad en cómputo era frecuentemente evitado. Y hasta cierto punto, esto era muy justificable: ¿Intrusos? ¿Integridad? ¿Validaciones? Conceptos que hoy a todos parecen fundamentales eran vistos como distracciones teóricas que sólo entorpecían la usabilidad de los sistemas. En la década de los 80 había muy poco software diseñado para su operación en red, y mucho menos para la idea de red que tenemos hoy en día. Y si bien es cierto que la mayor parte de los ataques se origina -y siempre se ha originado- dentro del perímetro de confianza de nuestra organización, hace 20 años sencillamente había menos información sensible alojada en medios electrónicos, menos gente con el conocimiento necesario para manipularla, e incluso la manipulación tenía efectos más nocivos: Si bien hace años la infraestructura de cómputo era el soporte facilitador, la copia maestra estaba en papel - Hoy en día estamos transitando hacia la situación opuesta, en que la versión electrónica es la primaria. Hoy, una intrusión en nuestros sistemas puede poner en jaque la integridad de la información primaria.

Mantener la seguridad en los sistemas que desarrollamos implica un alto costo: Los programadores tienen que aprender a evitar errores comunes; tenemos que concientizarnos y acostumbrarnos a dedicar recursos a implementar baterías de pruebas; tienen que entrar en juego validaciones y conversiones sobre los datos que manipulamos, con costos en tiempos de procesamiento de cada solicitud... Pero, afortunadamente, ha crecido también la conciencia de la importancia de destinar a la seguridad la atención y recursos que requiere.

El problema sigue siendo, coloquialmente... ¿con qué se come? La seguridad en cómputo sigue siendo un campo dificil de entender, con muchas aristas ocultas. Es por esto que en este artículo abordaremos algunos temas fundamentales, que a pesar de ser bien conocidos, siguen siendo origen de un sinfín de nuevos sistemas creados con obvios agujeros.

1. ¿Qué es la seguridad en cómputo?

A riesgo de que parezca perogrullada, un sistema seguro no es otra cosa que un sistema que responde como debe, un sistema que cubre las necesidades y requerimientos con que fue concebido. Claro, a esta pregunta hay que verla a la luz de varios criterios para que en verdad signifique algo. Intentemos nuevamente. Un sistema seguro presenta:

Consistencia
Ante las mismas circunstancias, debe presentar el mismo comportamiento. Ante un sistema seguro, el tradicional remedio "¿ya intentaste reniciarlo?" no surte efecto. Si una grandísima proporción de usuarios se ha acostumbrado a que un reinicio resuelve las cosas, no es sino porque el ambiente de ejecución se ensucia con elementos que debería haber descartado - Y por tanto, podemos concluir que sistemas en cuestión son inherentemente inseguros.
Protección y separación
Los datos, instrucciones y espacio de memoria de un programa, componente o usuario no deben interferir ni permitir interferencia de otros. Las condiciones anormales ocurridas en uno de los componentes -sean accidentales o expresas- deben tener un impacto mínimo en el sistema como un conjunto.
Autenticación
El sistema debe poseer los mecanismos necesarios para asegurarse que un usuario es realmente quien dice ser.
Control de acceso
Nuestro sistema debe poder controlar con toda la granularidad necesaria los permisos de acceso a sus datos - Quién tiene acceso a qué recursos, y qué tipo de acceso tiene.
Auditoría
El sistema debe ser capaz de registrar, así como de notificar a quien sea necesario, de cualquier anomalía o evento importante.

Claro está, todos estos atributos deben ir matizados, priorizándolos al nivel adecuado a nuestras necesidades. Ir demasiado lejos en uno de estos objetivos puede ser de hecho perjudicial para los fines de nuestro sistema - Por poner un ejemplo, es de todos bien conocido que el tradicional esquema de autenticación basado en usuario y contraseña es fácil de engañar; basta adivinar (o conseguir) un pedazo de información, típicamente de muy débil complejidad, para estar autenticado como determinado usuario. En México, desde hace algunos años, los bancos exigen la identificación del cliente a través de dispositivos que presenten una mucho mayor complejidad, generando cadenas de números que cambian periódicamente. Pero, obviamente, poca gente requerirá un nivel de seguridad similar a éste, o basado en parámetros biométricos, para abrir su cuenta de correo. Si bien es aceptable demandar que un usuario bancario tenga acceso al dispositivo como una medida de seguridad (lo cual es una inconveniencia, dado que muchos usuarios prefieren no cargarlo consigo constantemente - Precisamente pensando en la seguridad) es aceptable, requerir medidas similares para acceso al correo electrónico sería de inmediato criticado por todos los usuarios como excesivo y como limitante a la usabilidad.

Y otra anotación: Nos es natural aspirar a la perfección, al 100%. Sin embargo, dice el refrán que "lo perfecto es enemigo de lo bueno". Es importante que, en toda etapa de la planeación, desarrollo, implantación y tiempo de vida de un sistema recordemos que un 100% de seguridad es una utopía, un objetivo que sólo puede servir para guiar nuestro trabajo diario.

Los programas son escritos por humanos, y son también humanos quienes administran los sistemas en que corren. Hay una gran cantidad de interacciones entre los elementos de un programa y el sistema, y un cambio en cualquiera de ellos puede tener consecuencias inesperadas si no se hace con cuidado y conciencia. Constantemente aparecen nuevas categorías de errores capaces de llevar a problemas de seguridad. Parte fundamental de nuestra actuación como profesionales en nuestro campo debe ser el mantenernos al día con los últimos desarrollos y las últimas amenazas.

2. El estado actual de la seguridad en cómputo

En un estudio publicado a inicios del 2009, SANS y MITRE (SANS/MITRE, 2009) publicaron la lista de los 25 errores de seguridad más importantes en frecuencia y relevancia. Este listado incluye muchos temas fundamentales, que deberían ser comprendidos por cualquier programador que se diga profesional. Vienen explicados con un buen nivel de detalle, detallando cómo evitar o mitigar sus efectos. Éste estudio ha contado con amplio apoyo por parte de diversos sectores tanto de la academia como de la industria.

Sin embargo, volviendo al punto con el que inicia el presente texto: En realidad, ¿el hecho de que ahora la seguridad en cómputo sea un tema que forma parte del inconsicente colectivo realmente ha llevado al mejoramiento de la calidad general del código? Es muy posible que estemos ante un claro caso de falso sentido de seguridad.

Usuarios y programadores están al tanto del peligro que corren al utilizar un sistema informático. Obviamente, los programadores están (o por lo menos, deberían estar) más atentos, dado que se dedican profesionalmente al cómputo y que comprenden (o, nuevamente, deberían comprender) mucho mejor las interacciones que llevan a las principales vulnerabilidades. Sin embargo, una y otra vez nos topamos con sistemas con las mismas clases básicas de errores, de tremendos agujeros de seguridad a través de los cuales podrían pasar marchando regimientos completos.

¿Por qué sucede esto? En síntesis, podría resumirse en los siguientes factores:

Es por esto que resulta tan importante enfatizar en explicar y enseñar respecto a las principales vulnerabilidades de que al día de hoy hay que cuidarse.

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 y prevalente hoy por hoy es la de los sistemas Web. Al mismo tiempo, dado medio mismo a través del cual se transporta y por el ámbito global que han adquirido muchos de éstos sistemas, que encontraremos bajo éste modelo de desarrollo la categoría más expuesta directamente a elementos hostiles — Esta es la razón por la que el presente artículo adopta un enfoque principalmente orientado a éste tipo de sistemas.

Si bien hay una tremenda cantidad de categorías en las cuales podríamos ahondar, juzgamos que los tres casos presentados a continuación son una buena muestra de las vulnerabilidades más comunes, peligrosas, y aquellas de las cuales es más importante estar consciente y atento al programar — O al auditar sistemas existentes.

3. Ejemplo 1: Inyecciones de SQL

Las vulnerabilidades más comunes son también las más fáciles de explotar para un atacante - Y utilizando algunas prácticas base, son también las más fáciles de evitar o corregir: Casi todas ellas se originan en la falta de validación (o exceso de confianza) en los datos proporcionados por el usuario.

Prácticamente la totalidad de los sistemas desarrollados procesarán datos provenientes de terceros. Ya sea mostrando o grabando lo expresado en formas HTML, determinando el flujo de la aplicación a través de rutas y parámetros o «galletas» HTTP (ver sección 4), o incluso -considerando la tendencia de migración hacia un esquema de «cloud computing»- tomando resultados de procedimientos remotos en sistemas no controlados por nosotros, a cada paso es necesario emplear datos no confiables, o generados por una entidad no confiable.

Esta puerta de entrada permite a un atacante una amplia variedad de modalidades de intrusión. En general, podemos hablar de ellas como inyección de código interpretado - Dedicaremos nuestra atención a la inyección de SQL (Wikipedia, 2004-2009b).

En el desarrollo de sistemas debemos partir siempre del principio de mínima confianza: No debemos confiar en ningún dato proveniente de fuera de nuestro sistema, independientemente de quién sea el usuario. Esto es especialmente importante cuando requerimos que un elemento cruce entre las principales barreras de las diversas capas de nuestro sistema.

3.1 Comprendiendo la inyección

Tomaremos como ejemplo un URL típico generado por uno de los sistemas de administración de contenido (CMS) en boga hoy en día: Joomla. Por razones obvias, el nombre verdadero del sitio en cuestión ha sido reemplazado por «www.ejemplo.com».

http://www.ejemplo.com/content/view/825
Todos hemos analizado URLs, y resultará obvio que «825» corresponda al ID de la nota en la base de datos, y que los componentes «content» y «view» indiquen la operación que el sistema debe realizar ante una solicitud. Ahora bien, ¿a qué me refiero a que cruzamos las barreras entre las capas? ¿Y cuáles son las principales?

Enfoquémonos en el ID. Al analizar el URL, el ID es un pedazo de texto (formalmente es una cadena que es recibida como parte del método GET, uno de los métodos definidos para el protocolo HTTP). El servidor Web que recibe mi solicitud interpreta este método GET y encuentra -utilizando mod_rewrite, en caso de tratarse de un servidor Apache como la mayoría de los sitios de la red, a través de configuración típicamente indicada en el archivo .htaccess- que el contenido indicado por la ruta «/content/view/*» debe ser procesado por el archivo index.php, que a su vez (dada su terminación o demás reglas que pueden aplicarse) es manejado por el lenguaje PHP. El archivo index.php es provisto por el sistema Joomla, que reconoce la ruta, convierte al ID en su representación numérica y lo utiliza para pedir a la base de datos le entregue los datos relacionados con determinado artículo. Entonces, aquí podemos reconocer los siguientes puntos principales de manipulación de la solicitud:

La variabilidad de los primeros pasos es en realidad menor - Pero al solicitar a la base de datos el artículo «825» (y este es el caso base, el más sencillo de todos) deben pasar muchas cosas. Primero que nada, «825» es una cadena de caracteres. PHP es un lenguaje débilmente tipificado (los números se convierten en cadenas y viceversa automáticamente según sea requerido), pero una base de datos maneja tipos estrictamente.

Un atacante, una persona que quiere causar daño u obtener acceso mayor al cual tiene autorizado en nuestro sistema, basará su acercamiento en ser creativo respecto a cómo engañar a nuestro sistema: Es muy poco frecuente que busque adivinar usuarios/contraseñas; más bien, intentará engañar al sistema para entregar resultados distintos de aquello que parece estar siendo solicitado. ¿Y cómo se engaña a un sistema? Pidiéndole algo que no se espere - Por ejemplo, «825aaa». En este caso (¡felicidades!), el código PHP que invoca a la base de datos sí verifica que el tipo de datos sea correcto: Hace una conversión a entero, y descarta lo que sobra. Sin embargo, en muchos sistemas desarrollados a en casa, una solicitud similar lleva a un mensaje como el siguiente:

    Warning: pg_execute() [function.pg-execute]: Query failed: ERROR:
    invalid input syntax for integer: "825aaa" in
    /home/(...)/index.php on line 192 
Esto indica que uno de los parámetros fue pasado sin verificación de PHP al motor de base de datos, y fue éste el que reconoció al error.

Ahora, esto no califica aún como inyección de SQL (dado que el motor de bases de datos supo reaccionar ante esta situación), pero estamos prácticamente a las puertas. El código desarrollado por una determinada persona en un lapso de tiempo dado tiende a repetir muchos patrones — Es casi un hecho que si desarrollador no validó la entrada en un punto, habrá muchos otros en que no lo haya hecho. Este error en particular nos indica que el código construye la cadena de consulta SQL a través de una interpolación parecida a la siguiente:

    $id_art = $_GET['id'];
    $sql = "SELECT * FROM articulo WHERE id = $id_art"
La vulnerabilidad aquí consiste en que el programador no tomó en cuenta que $id_art puede contener cualquier cosa enviada por el usuario - Por el atacante en potencia. ¿Cómo puede aprovecharse esto? No hay límites más que la imaginación.

Presentamos a continuación algunos ejemplos, evitando enfocarnos a ningún lenguaje en específico - Lo importante es el proceso y el tratamiento que se da al SQL generado.

Para estos ejemplos, cambiemos un poco el caso de uso — Nuevamente, recordando que errores derivados del estilo de codificación detectados en un punto muy probablemente se repitan a lo largo del programa. En vez de ubicar recursos, hablemos acerca de una de las operaciones más comunes: La identificación de un usuario vía login y contraseña. Supongamos que el mismo sistema del código recién mencionado utiliza la siguiente función para validar a sus usuarios:

    $data = $db->fetch("SELECT id FROM usuarios WHERE login = '$login' AND passwd = '$passwd'");
    if ($data) {
        $uid = $data[0];
    } else {
        print "<h1>Usuario inválido!</h1>";
    }

Aquí pueden apreciar la práctica muy cómoda y común de interpolar variables dentro de una cadena - Muchos lenguajes permiten construir cadenas donde se expande el contenido de determinadas variables. En caso de que su lenguaje favorito no maneje esta característica, concatenar las sub-cadenas y las variables nos lleva al mismo efecto. Por ejemplo, en VisualBasic obtendríamos la misma vulnerabilidad construyendo nuestra cadena así:

    Dim sql as String = "SELECT id FROM usuarios WHERE login = '" & login & "' AND passwd = '" & passwd & "'"
Sin embargo... ¿Qué pasaría aquí si el usuario jugara un pequeño truco? Si solicitara, por ejemplo, entrar al sistema utilizando como login a «fulano';--», esto llevaría al sistema a ignorar lo que nos diera por contraseña: Estaríamos ejecutando la siguiente solicitud:
    SELECT id FROM usuarios WHERE login = 'fulano';--' AND PASSWD = ''
La clave de este ataque es confundir a la base de datos para aceptar comandos generados por el usuario - El ataque completo se limita a cuatro caracteres: «';--». Al cerrar la comilla e indicar (con el punto y coma) que termina el comando, la base de datos entiende que la solicitud se da por terminada, y cualquier cosa que siga es otro comando. Podríamos enviarle más de un comando consecutivo que concluyera de forma coherente, pero lo más sencillo es utilizar el doble guión indicando que inicia un comentario. De este modo, logramos vulnerar la seguridad del sistema, entrando como un usuario cuyo login conocemos, aún desconociendo su contraseña.

Pero podemos ir más allá - Siguiendo con este ejemplo, típicamente el ID del administrador de un sistema es el más bajo. Imaginen el resultado de los siguientes nombres de usuario falsos:

    ninguno' OR id = 1;--
«ninguno» no es un usuario válido del sistema, pero ésto resultaría en una sesión con privilegios de administrador.
    '; INSERT INTO usuarios (login, passwd) VALUES ('fulano', 'de tal'); --
Esto no otorgaría en un primer momento una sesión válida, pero crearía una nueva cuenta con los valores especificados. Obviamente, es posible que fuera necesario averiguar -a prueba y error- qué otros valores es necesario agregar para que ésto nos otorgue un usuario válido con suficientes privilegios.
    '; DROP TABLE usuarios; --
Un atacante puede darse por satisfecho con destruir nuestra información. En este caso, el atacante destruiría de un plumazo nuestra tabla de usuarios. Me es imposible dejar de sugerirles visitar al ya famoso «Bobby Tables» (Munroe, 2007).

3.2 Evitando las inyecciones de SQL

¿Y qué podemos hacer? Protegerse de inyección de SQL es sencillo, pero hay que hacerlo en prácticamente todas nuestras consultas, y convertir nuestra manera natural de escribir código en una segura.

La regla de oro es nunca cruzar fronteras incorporando datos no confiables - Y esto no sólo es muy sencillo, sino que muchas veces (específicamente cuando iteramos sobre un conjunto de valores, efectuando la misma consulta para cada uno de ellos) hará los tiempos de respuesta de nuestro sistema sensiblemente mejores. La respuesta es separar preparación y ejecución de las consultas. Al preparar una consulta, nuestro motor de bases de datos la compila y prepara las estructuras necesarias para recibir los parámetros a través de «placeholders», marcadores que serán substituídos por los valores que indiquemos en una solicitud posterior. Volvamos al ejemplo del login/contraseña. En este caso, presentamos el ejemplo como sería construído desde el lenguaje Perl:

    $query = $db->prepare('SELECT id FROM usuarios WHERE login = ? AND passwd = ?');
    $data = $query->execute($login, $passwd);
Los símbolos de interrogación son enviados como literales a nuestra base de datos, que sabe ya qué le pediremos y prepara los índices para respondernos. Podemos enviar contenido arbitrario como login y password, ya sin preocuparnos de si el motor lo intentará interpretar.

Algunos lenguajes y bibliotecas de acceso a bases de datos (notablemente, la popular combinación PHP+MySQL) no implementan la funcionalidad necesaria para separar los pasos de preparación y ejecución. La respuesta en dichos lenguajes es utilizar funciones que escapen explícitamente los caracteres que puedan ser nocivos. En el caso mencionado de PHP+MySQL, esta función sería mysql_real_escape_string (Achour, 1997-2009):

    $login = mysql_real_escape_string($_GET['login']);
    $passwd = mysql_real_escape_string($_GET['passwd']);
    $query = "SELECT id FROM usuarios WHERE login = '$login' AND passwd = ''";
    $data = $db->fetch($query);
Una gran desventaja que ésto conlleva es la cantidad de pasos que el programador debe efectuar manualmente — Especialmente cuando entran en juego mayores cantidades de datos, y hay que verificar a cada uno de ellos. Cabe mencionar que las bibliotecas que implementan conectividad a otras bases de datos para PHP (por ejemplo, PostgreSQL) sí ofrecen las facilidades necearias para la preparación y ejecución como dos pasos separados, con una semántica muy similar a la anteriormente descrita.

Revisar todas las cadenas que enviamos a nuestra base de datos puede parecer una tarea tediosa, pero ante la facilidad de encontrar y explotar este tipo de vulnerabilidades, bien vale la pena. En las referencias (Mavituna, 2007), (Microsoft, 2009), (Friedl, 2007) podrán encontrar información mucho más completa y detallada acerca de la anatomía de las inyecciones SQL, y diversas maneras de explotarlas aún incluso cuando existe cierto grado de validación.

3.3 Alternativa específica: El uso de ORMs

Otra alternativa interesante que todo programador debe conocer es el uso de marcos de abstracción, como los mapeos objeto-relacionales (ORMs (Wikipedia, 2007-2009c)). Estos marcos se encargan de crear todo el "pegamento" necesario para hermanar a dos mundos diferentes (el de las bases de datos relacionales, basados en registros almacenados en tablas, y el de la programación orientada a objetos, basado en objetos instanciados de clases), mundos que comparten algunas características (estructura homogénea a todos los elementos del mismo tipo) pero no otras (manejo de relaciones complejas como composición o agregación). Pero, en el ámbito aquí discutido, la principal característica de los ORMs es que se encargan de cubrir los detalles relativos a la integración de dos lenguajes y dos formas de ver al mundo muy distintas. Uno de los grandes atractivos de los ORMs es que, entre otras muchas ventajas, su uso nos puede liberar por completo (o en una gran medida) de escribir directamente código SQL en la totalidad de nuestro proyecto.

Una solicitud similar a la anterior a través del ORM ActiveRecord en el lenguaje Ruby sería sencillamente:

    usuario = Usuario.find_by_login(login, :conditions => 'passwd = ?', passwd)
En este caso, sería directamente la biblioteca la que convierte una llamada find a la clase Usuario (que hereda de ActiveRecord::Base) en una consulta SQL, escapa/limpia las entradas y envía la solicitud a la base de datos. Esto proporciona la ventaja adicional de que todo el código relativo al paso de parámetros a la base de datos está concentrado en un sólo punto (y en un punto ampliamente utilizado y escrutado por profesionales de todo el mundo); toda omisión o error que vaya siendo detectado llevará a que éste sea corregido en un sólo punto — Y nuestra aplicación recibirá, en todas las consultas que pasen a través del ORM, el beneficio de esta mejoría. Además, estando desarrollada verdaderamente en un sólo lenguaje, la aplicación se mantiene más limpia y resulta más fácil de comprender y depurar.

ActiveRecord es sólo uno de muchos ORMs existentes; está implementado para Ruby (Heinemeier, 2004-2009) y para .NET (Verissimo, 2003-2009), y hay implementaciones muy cercanas a este patrón en varios otros lenguajes.

4. Ejemplo 2: Cross-Site Scripting (XSS)

Como ya mencionamos, las inyecciones se presentan cuando cruzamos fronteras. Las inyecciones SQL no son, ni mucho menos, el único tipo de inyección de código malicioso; cualquier lugar en que un mismo dato puede significar cosas diferentes dependiendo del contexto en que es evaluado es susceptible a ser vulnerable a inyección. Las siguientes categorías las menciono únicamente para invitar al lector a empaparse en la problemática que conllevan.

Del mismo modo que debemos sanitizar (limpiar, escapar) los datos que llevamos hacia abajo a través de las capas del diseño de nuestro sistema, debemos hacer lo mismo al subir. ¿Qué significa esto? Que no sólo debemos proteger las capas privadas de nuestra aplicación, a las cuales un posible atacante no debería tener acceso directo, sino que debemos también proteger a las capas superiores — Aquellas que están incluso fuera de nuestro control — Como el navegador de nuestros demás usuarios (MITRE, 2008-2009b).

En un sistema Web (especialmente si queremos participar de eso a lo que llaman Web 2.0, muchas veces desplegaremos a un usuario información provista por otros usuarios. Para ofrecer toda la funcionalidad y respuesta ágil de un sitio moderno, los navegadores están típicamente configurados para ejecutar todo código JavaScript que reciben, confiando en quien lo origina. Ahora, ¿qué pasaría si un usuario malicioso deja en un blog el siguiente comentario?

    <script language="JavaScript">
        window.location="http://www.hackme.org/1234"; 
    </script>
En efecto, inmediatamente al cargar la página en cuestión, el usuario sería redirigido a un sitio diferente (y con justa razón: Para su navegador, ésta indicación viene del sitio, no puede saber que viene de un elemento hostil).

Éste sitio podría hacerse pasar por el sitio víctima, sin que el usuario se diera cuenta, y podría llevar al usuario a diferentes escenarios donde se le extrajera información confidencial - Por ejemplo, se le puede pedir que se vuelva a autenticar. La mayor parte de los usuarios caerán en el engaño, con lo que éste puede volverse un potente mecanismo para suplantación de identidad.

Por si no bastara, cada usuario, obviamente, tiene un perfil diferente — y normalmente, nivel o credenciales de acceso también diferentes. ¿Qué pasaría si el código JavaScript fuera muy ligeramente más malicioso? Por ejemplo (y nuevamente, meramente como ejemplo ficticio), podría hacer que un administrador le diera acceso completo al sitio. Si el atacante recibió el usuario número 152, podría enviar un mensaje privado al administrador del sitio que incluyera:

    <script language="JavaScript">
        window.location="/admin/user/152/edit?set_admin=1";
    </script>
Claro está, asignar una nueva dirección a window.location es probablemente la manera más burda y notoria para llevar a cabo estos ataques; hay muchas maneras más sigilosas, que pueden pasar completamente desapercibidas por un usuario casual.

Este tipo de ataques son conocidos genéricamente bajo el nombre de XSS o Cross-Site Scripting (MITRE, 2008-2009c). La clave para evitarlos es nuevamente sanitizar toda la información — Pero en este caso, toda la información a enviar al cliente. En el caso de HTML, prácticamente basta con escapar ciertas entidades (caracteres que pueden tener significados especiales). Por ejemplo, reemplazando todos los caracteres «<» por su representación «&lt;» y todos los caracteres «>» por «&gt;» (como primer acercamiento), éste código será desplegado de una manera limpia. Éste es típicamente un proceso aún más engorroso que sanitizar la entrada, por la cantidad de puntos donde hay que repetir la validación. Dependiendo del caso, muchos desarrolladores optan por limpiar a la entrada todo lo que será eventualmente desplegado — pero esto puede llevar al usuario a algunas condiciones en que el navegador no lo des-sanitiza, resultando sencillamente en una salida aparentemente llena de basura.

Como nota adicional: Es posible (y altamente deseable) sanitizar toda la información a la entrada, desechando todo lo que no sea claramente aceptable. Sin embargo, siempre hay casos en que requerimos guardar la información completa y sin manipular.

Si bien evitar XSS es más dificil que evitar inyecciones de SQL. A pesar de que el impacto de XSS, a primera vista, es menos severo que el de una inyección de SQL, éste puede tardar mucho más tiempo en ser corregido, puede estar presente en más puntos de nuestro código, y por sobre todo, es más tedioso de arreglar — Por lo que es fundamental acostumbrarnos a verificar toda la información que despleguemos a tiempo.

Además, si bien el impacto es menos inmediato, es típicamente más sigiloso. Si un atacante obtiene la información de acceso de una gran cantidad de usuarios, para propósitos prácticos no podemos volver a confiar en ninguno de nuestros usuarios — Todos pueden estar potencialmente controlados por el atacante.

5. Ejemplo 3: Manejo de sesiones a través de galletas HTTP

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, así como posibles cambios de estado del lado del servidor), 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, los verbos 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 (Costello, s.f.), principalmente orientado a servicios Web (interfaces expuestas vía HTTP, pero orientadas a ser consumidas/empleadas por otros programas, no por humanos), 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 el estado de los datos — Es aceptable que a través de un GET, por ejemplo, aumente el contador de visitas, (que haya un cambio no substantivo) — Pero muchos desarrolladores han sufrido por enlazar a través de un GET (como todo navegador responde a una liga HTML estándar), por ejemplo, el botón para eliminar cierto objeto.

¿Cuál es el peligro? 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.

HTTP fue concebido (Berners-Lee, 1996) como un protocolo a través del cual se solicitaría información estática. Al implementar las primeras aplicaciones sobre HTTP, nos topamos con que cada solicitud debía incluir la totalidad del estado. En términos de redes, TCP implementa exclusivamente la capa 4 del modelo OSI, y si bien 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. HTTP es un protocolo puramente capa 6 (omite toda información relacionada con la sesión y sirve únicamente para presentar ya sea la aplicación o el contenido estático). 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 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. Éste mecanismo indica que todas las galletas que defina un servidor a determinado cliente serán enviadas en los encabezados de cada solicitud que éste le realice, por lo que se recomienda mantenerla corta — Atendiendo a esta recomendación, varias implementaciones de galletas no soportan más de 4KB. 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 (Kristol, Montulli, 1997) y (Kristol, Montulli, 2000). 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 su sesión en el banco); sin embargo, todo sistema puede solicitar galletas arbitrarias guardadas en el cliente. Para cada servidor, pueden almacenarse 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 enviada 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 de esto, es recomendado guardar un token (literalmente, ficha o símbolo) que apunte a la información. Esto es, en vez de guardar el ID de un usuario, debe enviarse una cadena criptográficamente fuerte (Wikipedia, 2004-2009d) que apunte a un registro en la base de datos del servidor. ¿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) (Wikipedia, 2001-2009e). 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 serviría enviar, por ejemplo, el ID de la sesión - Esto nos dejarí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 (mecanismo conocido como «rainbow tables», tablas arcoíris; hay varios proyectos [10] que han construido tablas para diversas aplicaciones, como la recuperación de contraseñas en sistemas Windows). 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: Las galletas son muchas veces vistas como un peligro por los activistas de la privacidad y el anonimato, dado que permiten crear un perfil de las páginas que va visitando un usuario (especialmente en el caso de empresas como Google o DoubleClick, que han sido especialmente exitosas en ofrecer herramientas de anuncios o de monitoreo/estadísticas a muy diversos administradores de sitios en todo el mundo). Es importante recordar que algunas personas 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.

6. Conclusiones

A lo largo de este artículo revisamos tres de las principales categorías de vulnerabilidades/errores de programación que, a juicio del autor, más prevalentes y peligrosas resultan hoy en día. Ahora bien, no es casualidad que el encabezado de cada una de las secciones correspondientes fuera «Ejemplo» — Con este artículo no implico que baste con estar conciente y alerta ante éstas amenazas para dejar de lado las demás.

La seguridad en cómputo, si bien es un campo fascinante, es un campo que requiere de constante actualización. Además, si bien éste artículo se enfoca a las vulnerabilidades más comunes en servicios Web, no es ni busca ser comprehensivos — Hay amplísimas categorías a las cuales no nos acercamos siquiera, y eso no debe leerse como que carezcan de importancia — Las tres categorías analizadas son meramente ejemplos de lo que hay que tener en mente.

Ser desarrollador de sistemas es una profesión demandante. Para cumplirla a cabalidad, requiere estar al día en lo tocante a seguridad — No estarlo significa una irresponsabilidad ante usuarios o clientes de nuestro desarrollo.

La sociedad depende más que nunca de la sistematización de los procesos y de la capacidad de sustentarlos a distancia. Los programadores, y muy en especial los programadores de sistemas en red, se han vuelto piezas fundamentales del tejido social. Es fundamental que reconozcamos y aceptemos nuestro nuevo rol responsablemente. Es fundamental que quienes nos decimos y nos sentimos desarrolladores de sistemas comprendamos la importancia de nuestro trabajo para la sociedad toda, y actuemos en consecuencia.


BIBLIOGRAFÍA