sábado, 23 de agosto de 2008

Inyección de SQL (SQL Injection)

Uno de los temas a los cuales puede asistir en las “Primeras Jornadas Latinoamericanas de Arquitectura de Software” fue “Seguridad y Arquitectura de Software”, el cual fue presentado por Juan Carlos Herrera.

Dentro de los tópicos de la presentación, se menciono la "Inyección de SQL", como uno de los problemas que comúnmente se presentan en la seguridad del desarrollo de software. Pero….. que es la inyección de SLQ?.

Se define la inyección de SQL, como el proceso de insertar código SQL dentro de otro código SQL, con el objetivo de alterar el funcionamiento normal de una sentencia SQL.

Este problema no se debe a que el lenguaje de programación que estemos utilizando presente este problema de seguridad, sino mas bien, gracias a nosotros, los desarrolladores, que en la mayoría de los casos, tenemos un escaso o nulo filtrado de los datos en el proceso de validación de estos dentro de nuestras aplicaciones, lo que permite a un atacante, insertar (o mejor dicho “inyectar”) código SQL dentro de las variables de nuestra aplicación, las que utilizamos para realizar consultas SQL.

Pero como sucede esto? Por qué sucede esto? Donde está el problema?

Imaginemos que estamos desarrollando un software para una institución financiera (reconozco que no soy muy original para mis ejemplos), en donde la seguridad es vital. Como todo sistema bancario, este dispone de una base de datos donde almacena la información de sus clientes, sus cuentas, las transacciones bancarias, etc. Entonces, si quisiéramos consultar la información de los tipos de cliente (persona o empresa), podríamos utilizar una sentencia SQL como esta:

SELECT * FROM cliente WHERE tipoCliente=’tipoCliente’;

Donde la variable ‘tipoCliente’ contiene el tipo de cliente que es para el banco (persona o empresa).

Con esta simple consulta podríamos obtener todos los clientes de tipo “persona” que tenga el banco.

Imaginemos que además de consultar la información de un cliente por su tipo, queremos filtrar la información por comuna. Es decir, queremos ofrecerle al usuario la posibilidad de elegir, el tipo de cliente que quiere ver asociado a una determinada comuna.

Para esto, podríamos construir un servlet que procese esta información y cuyo resultado se despliegue en la página “consultaCliente.jsp”. Entonces, si el usuario realizaría una petición HTTP a nuestra JSP, pasándole los valores de la consulta por GET:

http://www.mibanco.cl/consultaCliente.jsp? tipoCliente =persona&comuna=macul

El servlet que procese esta petición, tomara los parámetros enviados por el usuario y ejecutaría la siguiente consulta SQL sobre la base de datos:

SELECT * FROM cliente WHERE tipoCliente=’persona’ AND comuna='macul';

Una vez ejecutada la consulta, los resultados obtenidos serian desplegados en la página “consultaCliente.jsp”.

Lo importante de todo esto, es que en algún punto de nuestro código, nosotros construimos la sentencia SQL en base a los valores enviados por el usuario. Es decir, si por algún motivo, al usuario se le ocurre enviarnos el valor “cualquiercosa” como comuna, nada se lo impedirá, y nuestra aplicación construirá la siguiente sentencia SQL:

SELECT * FROM cliente WHERE tipoCliente=’persona’ AND comuna='cualquiercosa';

La pregunta es, donde está el problema en todo esto?......al parecer, no existe ningún problema, porque en el peor de los caso, esta consulta no devolverá nada, ya que la comuna “cualquiercosa” no existe. Pero…….¿qué pasaría ahora si en vez de ingresar un valor cualquiera, el usuario se le ocurriera ingresar como comuna parte de una sentencia SQL?.

Por ejemplo, que sucedería en el caso de que el usuario enviara la siguiente consulta HTTP:

http://www.mibanco.cl/consultaCliente.jsp? tipoCliente =persona&comuna= %20OR%20%22=

Ahora nuestra aplicación construiría la siguiente consulta SQL:

SELECT * FROM cliente WHERE tipoCliente=’persona’ AND comuna =’’ OR ''=’’;

Estos si es un problema!!!! Estamos permitiendo que el usuario pueda manipular la sentencia SQL. Nuestra sentencia estaría diciéndole a la base de datos que le entregue todos los clientes de tipo persona y cuya comuna sea igual a ‘ ” ‘ o la expresión ‘=’ sea verdadera. Que en pocas palabras, le está diciendo a la base de datos que le entregue todos los clientes no importando cual sea la comuna.

Como podemos ver, el usuario está logrando alterar nuestra consulta para obtener un resultado no esperado por la aplicación. En este caso, mostrar todos los clientes de nuestro banco sin importar su comuna.

A este proceso se le conoce como”Inyección de SQL”. Si bien este pequeño ejemplo no implica un mayor “peligro” para nuestro sistema, no podemos pasar por alto que hay una vulnerabilidad grave en nuestro sistema, que podría permitir a un atacante con un poco más de conocimiento hacer otro tipo de inyecciones más perjudiciales para nuestra aplicación.

Qué pasaría con un usuario más astuto? Imaginemos que ahora nuestro usuario tiene un alto conocimiento de SQL (algún miembro del JUG Chile), y quiere validar si nuestro sistema presenta algún problema de seguridad frente a ataques de tipo “Inyección de SQL”. Entonces, no haya nada mejor, que enviar ciertos parámetros por la URL que le permitan obtener los datos de otra tabla, como por ejemplo, la tabla “cuentas”, que contiene toda la información de las cuentas bancarias de nuestros clientes.

Entonces, esta vez nuestro usuario realiza la siguiente consulta:

http://www.mibanco.cl/consultaCliente.jsp? tipoCliente =persona&comuna=%20UNION%20SELECT%20*%20FROM%20cuentas%20WHERE%20%22=

Entonces, nuevamente nuestro servlet que procesa esta petición, tomara los parámetros enviados por el usuario y ejecutara la siguiente consulta SQL sobre la base de datos:

SELECT * FROM clientes WHERE tipoCliente='persona' AND comuna='' UNION SELECT * FROM cuentas WHERE ''='';

Ahora si estamos en serios problemas!!!!!. El usuario a logrado obtener a través de esta consulta, todos los datos de las cuentas bancarias registradas en nuestra base de datos.

Entonces, nuevamente volvemos al punto inicial. Sera que el lenguaje que estamos utilizando para programa nuestro sistema presenta este tipo de problemas de seguridad? O será acaso un problema de nuestra base de datos?; y la pregunta principal….¿Puedo yo, como desarrollador de este sistema, evitar de alguna manera este tipo de ataques?.

Si analizamos nuestra aplicación, nos encontraremos que el principal problema radica en la nula verificación de los datos enviados por el usuario antes de realizar la consulta SQL a la base de datos.

Pero qué tipo de verificación?....bueno, simplemente validar que dentro de los valores enviados por el usuario, estos no contengan código SQL que permitan la manipulación de nuestra sentencia SQL.

Si analizamos nuestro ejemplo, los valores que tendríamos que verificar, son “tipoCliente” y “comuna”. Son estos valores, los que tendríamos que validar, verificando que no contengan caracteres especiales, como por ejemplo comillas, que puedan servir al usuario para inyectar código SQL en la consulta.

En este caso, nos bastaría con verificar que el “tipoCliente” y la “comuna”, sólo contengan letras, sin ningún carácter especial.

Lo importante de todo esto, es que el problema de la seguridad de nuestro sistema (debido a la inyección de SLQ), radica principalmente en la verificación de datos dentro de nuestro sistema. Por lo tanto, una solución, es filtrar siempre la entrada del usuario, de manera que atacante, nunca pueda inyectar código SQL en ningún lado dentro de nuestra aplicación.

Algunas técnicas, comúnmente recomendadas para la filtración de los valores enviados por el usuario son:

  • Limitar, siempre que se pueda, el tamaño del valor ingresado. De esta manera, limitamos la cantidad de código SQL que un atacante puede inyectar. En especial si se trata de una consulta anidada.
  • No permitir, o en su defecto escapar, todos los caracteres que puedan llegar a ser usados como parte de una inyección SQL: comilla simple (’), punto y coma (;) y caracteres de comentario (/* */ y –).
  • Filtrar con expresiones regulares todo lo que ingrese el usuario. Por ejemplo, en un formulario de registro, el campo mail sólo debería aceptar letras en minúsculas, números y un arroba. Al igual que en un número de teléfono sólo deberían aceptarse números.

Otros consejos, que no se encuentran relacionados con al verificación de los valores son:

  • Realizar las consultas de la aplicación, utilizando un usuario, con la menor cantidad de privilegios posible dentro de la base de datos. De modo que si un atacante, pudiera saltarse de algún modo la verificación de los valores e inyectar código en alguna de nuestras consultas, los daños se reduzcan al no tener suficientes privilegios.
  • Realizar la verificación de los valores ingresados por el usuario, en todas las capas de la aplicación. Algunos desarrolladores por ejemplo, hacen una sola verificación dentro de la capa de presentación, utilizando JavaScript. Esto, puede ser fácilmente evitado, enviado los valores directamente a través del navegador.
  • No mostrar los mensajes de error devueltos por la base de datos. Para un atacante podría serle muy útil esta información, en especial para hacer “Blind Injection”.
  • Usar Prepared Statements: Esta es una de las mejores soluciones que se pueden utilizar para evitar este tipo de ataques, ya que prácticamente son inmunes a las inyecciones SQL.

Una explicación más detalla de la inyección de SQL y artículos relacionados con el tema, la pueden encontrar aquí.

Pero como todo problema en el mundo Java, siempre existe alguien con una solución. Para este tipo de ataques tenemos el framework HDIV (HTTP Data Integrity Validator).

2 comentarios:

Jc dijo...

Excelente post, queria postear al respecto pero me daba una lata enorme introducir el tema. Ahora lo hare linkeando tu post y concentrandome en q lo que realmente todo el mundo deberia saber, que mas que un problema de seguridad, es un problema de diseño de software y esta para cortarle los dedos (para comenzar) a los desarrolladores que hacen esto, a menos que no sepan claro.

Benoit dijo...

Gracias por este post muy interesante.

Me parece que utilizando NamedQueries de JPA, el riesgo disminuye mucho, ¿cierto?

NamedQueries generan PreparedStatements (a lo menos en las implementaciones TopLink y Hibernate que conozco) y, como lo dices en tu post, es una de las mejores opciones contra inyección de SQL.