Quizás el principal método de protección contra SQL Injection sea la utilización de Procedimientos Almacenados para realizar todos los accesos a base de datos (al menos en el caso de SQL Server y SQL Injection). Sin embargo, no es suficiente con utilizar Procedimientos Almacenados, ya que en función de cómo utilicemos los Procedimientos Almacenados en nuestra aplicación conseguiremos protegernos de SQL Injection o seguiremos al descubierto ante ataques de SQL Injection.
A continuación se ilustran dos ejemplos de utilización de Procedimientos Almacenados para acceder a SQL Server desde una Aplicación ASP, teniéndose en cuenta que estos ejemplos son aplicables tanto a Aplicaciones Windows (ej: aplicaciones Visual Basic 6) como a Aplicaciones .Net (tanto WebForms como WinForms), y cualquier otro tipo de aplicaciones (aplicaciones Java, componentes COM+, etc.).
Para la realización de estos ejercicios, hemos creado en el Laboratorio de GuilleSQL un Procedimiento Almacenado en SQL Server, y dos páginas ASP: una página ASP llamará al Procedimiento Almacenado sin utilizar Consultas Parametrizadas, y las otra página ASP llamará al Procedimiento Almacenado utilizando Consultas Parametrizadas. ¿Qué diferencias encontraremos? Sigue leyendo...
Ejemplo 1. Utilizar Procedimientos Almacenados sin Consultas Parametrizadas
En este primer ejemplo, y continuando con el escenario utilizado anteriormente en este artículo (un formulario Web de inicio de sesión en una Aplicación ASP), vamos a utilizar el mismo código empleado en los ejemplos anteriores del Laboratorio GuilleSQL, con la única diferencia de que el acceso a la base de datos lo realizaremos a través de una llamada a un Procedimiento Almacenado (en el ejemplo, el procedimiento spGetUsuario). Eso sí, en este primer ejemplo no vamos a utilizar Consultas Parametrizadas, por lo tanto, nos vamos a limitar a sustituir el texto de la sentencia SELECT utilizada anteriormente por el texto de la llamada al Procedimiento Almacenado. Esto es lo equivalente a utilizar en java el interfaz java.sql.Statement. El código resultante es el siguiente:
<% strUsuario = Request.Form("txtUsuario") strPassword = Request.Form("txtPassword")
strSQL = "GuilleSQL.spGetUsuario '" & strUsuario & "', '" & strPassword & "'"
Set miCon = Server.CreateObject("ADODB.Connection") miCon.ConnectionString = "Provider=SQLOLEDB;Data Source=GuilleXP;trusted_connection=yes;Initial Catalog=GuilleSQL" miCon.Open
Set miRS = Server.CreateObject("ADODB.Recordset") miRS.Open strSQL, miCon
'**** '**** (c) www.guillesql.es '**** Controlar aquí si existe el Usuario con miRS.EOF en un IF '**** y tomar los datos del Usuario (desde miRS), si miRS.EOF es falso '****
miRS.Close miCon.Close
Set miRS = Nothing Set miCon = Nothing %> |
Como se puede observar, seguimos ante el mismo problema, ya que al fin y al cabo seguimos construyendo una cadena de texto dinámicamente que seguidamente será ejecutada por SQL Server (o por el motor de base de datos que estemos utilizando), y en consecuencia, seguimos siendo vulnerables a la introducción de cadenas peligrosas por parte del atacante (Hacker).
También es cierto, que el simple hecho de utilizar Procedimientos Almacenados, tiene ciertas diferencias de comportamiento, que nos protege de algunos de los casos que vimos anteriormente (más o menos). Así, si volvemos a unos de los ejemplos anteriores, en que un usuario introduce los siguientes valores de entrada:
- txtUsuario: ' OR 'A'='A
- txtPassword: ' OR 'A'='A
Si sustituimos estos valores en la expresión que se utiliza para generar la consulta SQL que se ejecutará contra la base de datos, obtendremos la siguiente cadena:
GuilleSQL.spGetUsuario '' OR 'A'='A', '' OR 'A'='A' |
En este caso, el atacante ha fracasado, pues realmente se está intentando ejecutar una cadena sintácticamente incorrecta, lo cual producirá un error en el acceso a SQL Server (y así lo hemos comprobado en el Laboratorio de GuilleSQL). En consecuencia, nos hemos protegido ante el ataque (malamente, pues ha sido pagando el precio de conseguir un error de sintaxis de SQL Server). Evidentemente, resulta vital una correcta gestión de errores, y sobre todo, evitar mostrar los errores de acceso a base de datos al Usuario, ya que esto podría ser utilizado por un Usuario atacante (Hacker) como método para revelar parcial o totalmente el esquema y configuración de la base de datos que está atacando, y así poder afinar sus siguientes intentos de ataque.
Desde este punto de vista, parece que hemos mejorado (muy poco, por cierto), pero esto no es del todo cierto, ya que el atacante (Hacker) simplemente deberá de utilizar otro tipo de expresiones para aprovechar nuestra vulnerabilidad y progresar en su ataque. Supongamos que un usuario introduce los siguientes valores de entrada:
- txtUsuario: loquesea
- txtPassword: '; DROP TABLE GuilleSQL.USUARIOS; --
Entonces, si sustituimos estos valores en la expresión que se utiliza para generar la consulta SQL que se ejecutará contra la base de datos, obtendremos la siguiente consulta:
GuilleSQL.spGetUsuario 'loquesea', ''; DROP TABLE GuilleSQL.USUARIOS; --' |
En este nuevo ejemplo, el ataque del usuario (Hacker) progresará y tendrá éxito (si el usuario o login utilizado para acceder a SQL Server tiene permisos suficientes, claro ;-). Si examinamos la consulta SQL que será ejecutada por SQL Server, podemos identificar claramente tres sentencias SQL:
- Llamada al procedimiento GuilleSQL.spGetUsuario que no devolverá filas: GuilleSQL.spGetUsuario 'loquesea','';
- Consulta SQL maliciosa e invasora: DROP TABLE GuilleSQL.USUARIOS;
- Un simple comentario, para evitar errores de sintaxis en el ataque: --'
Con este ejemplo, conseguimos demostrar de forma empírica que aún utilizando Procedimientos Almacenados, seguimos siendo vulnerables a ataques de SQL Injection, al menos en el caso de no utilizar Consultas Parametrizadas.
Ejemplo 2. Utilizar Procedimientos Almacenados con Consultas Parametrizadas
En este segundo ejemplo, vamos a realizar el acceso a la base de datos a través de una llamada a un Procedimiento Almacenado, pero a diferencia del caso anterior, en este caso vamos a utilizar Consultas Parametrizadas. Con este método, evitamos tener que construir la consulta SQL como una concatenación de varias cadenas (es aquí dónde está el peligro), de tal modo, que al utilizar Consultas Parametrizadas y Procedimientos Almacenados se realizará una simple sustitución en los valores de los parámetros al realizar la llamada al Procedimiento Almacenado (sin concatenaciones, es decir, sin peligro). Además, como veremos en las pruebas realizadas en el Laboratorio de GuilleSQL, la utilización de parámetros nos permite introducir implícitamente algunas validaciones de tipo y rango. Esto es lo equivalente a utilizar en java el interfaz java.sql.PreparedStatement. El código resultante es el siguiente:
<% strUsuario = Request.Form("txtUsuario") strPassword = Request.Form("txtPassword")
Set miCon = Server.CreateObject("ADODB.Connection") miCon.ConnectionString = "Provider=SQLOLEDB;Data Source=GuilleXP;trusted_connection=yes;Initial Catalog=GuilleSQL" miCon.Open
Set miCom = Server.CreateObject("ADODB.Command") Set miCom.Activeconnection = miCon
miCom.commandText="GuilleSQL.spGetUsuario" miCom.commandtype = 4 miCom.Parameters.Append miCom.CreateParameter("@USU_ID",3,1,,1) miCom.Parameters.Append miCom.CreateParameter("@USU_PWD",3,1,,12)
Set miRS = miCom.Execute
'**** '**** (c) www.guillesql.es '**** Controlar aquí si existe el Usuario con miRS.EOF en un IF '**** y tomar los datos del Usuario (desde miRS), si miRS.EOF es falso '****
miRS.Close miCon.Close
Set miRS = Nothing Set miCon = Nothing %>
|
En esta ocasión sí que estamos protegidos ante ataques SQL Injection, ya que no construimos dinámicamente la cadena de texto SQL a ejecutar (eso sí, estamos protegidos siempre y cuando el procedimiento almacenado no utilice SQL Dinámico - requisito indispensable - , pues en dicho caso seguiremos bajo riesgo). Así, si volvemos a uno de los ejemplos anteriores, en que un usuario introduce los siguientes valores de entrada:
- txtUsuario: ' OR 'A'='A
- txtPassword: ' OR 'A'='A
La llamada al procedimiento almacenado no devolverá filas (comprobado en el Laboratorio de GuilleSQL), excepto que realmente existe un usuario con nombre ' OR 'A'='A y contraseña ' OR 'A'='A, con lo cual conseguimos demostrar que realmente ahora sí que estamos protegidos frente a ataques de SQL Injection. El atacante (Hacker) no ha conseguido su cometido, pues estaba intentando debilitar una consulta SQL con la introducción maliciosa de disyunciones lógicas (cláusulas OR) en la cláusula WHERE, y no ha conseguido el efecto que esperaba.
Es importante recordar que la utilización de parámetros implica la realización implícita de comprobaciones de tipo y rango. Por ejemplo, si especificamos una cadena demasiado larga (es decir, el texto introducido es más largo que la longitud máximo del parámetro - ej: VARCHAR de 20) se obtendrá un error como el siguiente:
ADODB.Command (0x800A0D5D)
La aplicación utiliza un valor de tipo no válido para la operación actual.
En inglés:
ADODB.Command (0x800A0D5D)
Application uses a value of the wrong type for the current operation.
Evidentemente, vuelve a resultar vital una correcta gestión de errores, y sigue siendo muy recomendable evitar mostrar los errores de acceso a base de datos al Usuario.
Conclusiones
Con todo esto, queda claro que no es suficiente con utilizar Procedimientos Almacenados para acceder a nuestra base de datos SQL Server, sino que además debemos acceder a dichos Procedimientos Almacenados a través de Consultas Parametrizadas, pues en caso contrario seguiremos al descubierto frente a ataques SQL Injection.
Pero la historia no acaba aquí, ya que el código existente dentro del Procedimiento Almacenado también podría ser nuestro talón de Aquiles, como es el caso de la utilización de código SQL Dinámico dentro del Procedimiento Almacenado. En este caso, si utilizamos SQL Dinámico dentro del Procedimiento Almacenado, y para construir la sentencia SQL que deseamos ejecutar recurrimos a la concatenación de variables, volveremos a estar en peligro, debiendo utilizar mecanismos adicionales para protegernos de ataques SQL Injection, como se explica más adelante en este artículo (ej: validar los datos de entrada, sustituir cadenas peligrosas, etc.).
Conclusión: para protegernos de ataques SQL Injection es recomendable utilizar Procedimientos Almacenados para todos los accesos a base de datos, accediendo a dichos Procedimientos Almacenados a través de Consultas Parametrizadas (desde ADO, ADO.Net, etc.), y evitando la utilización de SQL Dinámico en el interior de dichos Procedimientos Almacenados. En caso de utilizar código SQL Dinámico en el interior de dichos Procedimientos Almacenados, será necesario utilizar mecanismos adicionales para protegernos de ataques SQL Injection artículo (ej: validar los datos de entrada, sustituir cadenas peligrosas, etc.).