Search

Search this site:

Evitando las Inyecciones de SQL

En la edición anterior de SoftwareGurú prometí que en esta columna trataría temas relativos a la seguridad en cómputo, a cómo escribir código más confiable y más robusto.

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 que nos proporciona el usuario.

Prácticamente la totalidad de los sistemas que desarrollemos procesarán datos provenientes de terceros. Ya sea mostrando o grabando lo expresado en formas HTML, determinando el flujo de nuestra aplicación a través de rutas y parámetros o «galletas» HTTP, 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 debemos emplear datos en los que no confiamos.

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 - Y en esta ocasión hablaremos específicamente de inyección de SQL.

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.

Tomemos como primer ejemplo al sistema de gestión de contenido que usa SoftwareGurú. Si quisieran leer la edición anterior de esta columna, a la que hice referencia hace algunas líneas, pueden encontrarla en:

http://www.sg.com.mx/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 Apache que recibe mi solicitud interpreta este método GET y encuentra -utilizando mod_rewrite, indicado por el archivo htaccess- que el contenido indicado por la ruta /content/view/* debe ser procesado por el archivo index.php, que a su vez es manejado por el lenguaje PHP. El archivo index.php corresponde en este caso al 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. Como atacante, puedo buscar qué pasa si le pido al sistema 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 (y no doy URLs por razones obvias), en muchas ocasiones esto me llevaría a recibir 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. Podemos generalizar que cuando un 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 contiene alguna construcción parecida a la siguiente:

"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 puedo aprovecharme de esto? La imaginación es lo único que me limita.

Presentaré a continuación algunos ejemplos, evitando enfocarme a ningún lenguaje en específico - Lo importante es cómo tratamos al SQL generado.

Para estos ejemplos, cambiemos un poco el caso de uso: 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.

Sin embargo… ¿Qué pasaría aquí si el usuario nos jugara un pequeño truco? Si nos dijera, por ejemplo, que el login es «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 lo 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:

No puedo dejar de sugerirles visitar al ya famoso «Bobby Tables»[fn]Bobby Tables - http://www.xkcd.com/327[/fn].

¿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:

$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.

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 a continuación podrán leer mucho más acerca de la anatomía de las inyecciones SQL, y diversas maneras de explotarlas incluso cuando existe cierto grado de validación.

El tema de la inyección de código da mucho más de qué hablar - Lo abordaremos en la próxima columna, desde otro punto de vista.

Referencias:

Attachments

200905_softwareguru_1.jpg (698 KB)

200905_softwareguru_2.jpg (674 KB)