Es algo antiguo, sabido y, por tanto, todos deberíamos de estar tomando las debidas precauciones en nuestras aplicaciones, independientemente de cuál sea la plataforma de despliegue, para evitar los ataques de inyecciones SQL que podrían tener consecuencias muy graves: desde dejar expuestos los datos almacenados en las bases de datos a, sencillamente, permitir el borrado parcial o completo de la(s) bases de datos utilizada(s) por la aplicación (entre otras posibles operaciones).
Y más aún cuando Xojo pone en nuestras manos la forma de evitar de forma tremendamente sencilla los ataques de inyecciones SQL; se trata de utilizar los PreparedStatements asociados a la clase de base de datos que estemos utilizando: SQLite, MySQL o PostgreSQL, por citar algunos de los motores de bases de datos soportados por Xojo. Mediante dicho mecanismo podremos llevar precisamente la tarea de sanear los datos de entrada proporcionados por el usuario; siendo este uno de los principales pilares en la seguridad de nuestras aplicaciones.
A continuación veremos de qué forma se puede realizar este tipo de ataque de inyecciones SQL cuando no utilizamos los PreparedStatements para validar y sanear la entrada de datos y, posteriormente, veremos de qué modo tan sencillo podemos poner coto a este problema.
SQLite y SQLExecute
Puedes utilizar cualquier tipo de base de datos que no sea vital para ti o de la que dispongas de una copia de seguridad; o bien puedes utilizar la empleada en este ejemplo: Chinook Database (renombrada en mi caso a “test.sqlite”).
Abre Xojo y crea un proyecto de ejemplo (por ejemplo de escritorio), selecciona el objeto “Window1” y añade una propiedad con el nombre “BBDD” y tipo “SQLitedatabase“. A continuación añade el evento Open también sobre el objeto Window1, siendo aquí donde escribiremos el código encargado de conectar con nuestra base de datos de ejemplo. Todo el código necesario sería este:
dim f as FolderItem = SpecialFolder.Desktop.Child("test.sqlite") bbdd = new SQLiteDatabase if f <> nil then bbdd.DatabaseFile = f if not bbdd.Connect then MsgBox("I couldn't connect to the Database") end
Ten en cuenta que en este caso he utilizado la ruta en la que se encuentra mi base de datos de ejemplo, haciendo para ello uso de la clase SpecialFolder. Por tanto deberás de cambiar dicha línea (obtención del FolderItem) para adecuarla a la ruta en la que se encuentre tu archivo de base de datos a utilizar como ejemplo.
Ahora nos encargaremos de preparar una interfaz de usuario mínima para nuestras pruebas. Nada sofisticado, simplemente un campo de texto (objeto TextField) que nos servirá para introducir los textos que deseamos insertar en la base de datos de ejemplo, y un botón (PushButton) en cuyo evento Action será donde incluyamos el código propiamente dicho encargado de ejecutar las instrucciones SQL contra la base de datos.
Por ejemplo, la interfaz de usuario podría tener perfectamente el siguiente aspecto:
Antes de que podamos ejecutar nuestro ejemplo, y ver sus dañinas consecuencias, sólo nos queda por añadir el evento Action sobre el botón PushButton1 que hemos añadido previamente, e introducir el siguiente código:
if bbdd <> nil then bbdd.SQLExecute("insert into album(title, artistid) values('"+TextField1.Text+"','0')") end if
Como puedes ver, la instrucción SQL es bastante sencilla. En este caso estamos utilizando el método SQLExecute para que ejecute sobre la base de datos la sentencia SQL que le estamos pasando: insertar en los campos “title” y “artistid” de la tabla “album”, respectivamente, los valores obtenidos en el campo de texto de nuestra interfaz de usuario (tal cual, sin tratar), y el valor constante 0, simplemente porque esta tabla de la base de datos de ejemplo requiere el paso de un valor no nulo sobre dicho campo cada vez que deseemos crear un nuevo registro.
Ejecuta la aplicación, introduce cualquier valor en el campo de texto y pulsa el botón. Si inspeccionas la tabla de la base de datos de ejemplo mediante cualquiera de las múltiples herramientas disponibles, podrás ver que se ha añadido un nuevo registro cuyos valores se corresponderán al que hayas introducido en el campo de texto y el valor “0”.
Inyecciones SQL
Ahora es el momento de que veamos cuan sencillo resulta explotar la vulnerabilidad cuando realizamos las consultas SQL sin sanear previamente los datos de entrada. Vuelve a ejecutar la aplicación e introduce en esta ocasión el siguiente texto en el campo de entrada:
Nuevo valor','0'; drop table customers;
Si observas con atención la instrucción SQL original (la ejecutada mediante el método SQLExecute), verás que entrecomilla los valores asignados a cada uno de los campos. Pues bien, simplemente con introducir una comilla en el campo de texto estaremos alterando la instrucción original; de modo que a partir de ese punto estaremos en control para añadir como parte de la sentencia SQL a ejecutar cualquier otro comando que deseemos separado mediante el caracter punto y coma. En este caso se trata de borrar toda una tabla del esquema original de la base de datos mediante la instrucción “drop table customers”.
Pulsa sobre el botón y consulta a continuación la base de datos de ejemplo, comprobarás que en efecto se ha eliminado por completo la tabla Customers.
¡Prepared Statement al rescate!
La forma de evitar las inyecciones SQL en nuestras aplicaciones Xojo es bien fácil, aunque ligeramente más laboriosa por la cantidad de código adicional que hemos de escribir en comparación con la simple línea que hemos visto para ejecutar la instrucción SQL encargada de añadir un nuevo registro. Aun así, y vistas las implicaciones, está claro que merece muy mucho la pena hacerlo tal y como veremos a continuación. Borra el código del evento Action correspondiente al control PushButton1 y sustitúyelo por el que veremos a continuación.
El primero de los pasos a la hora de utilizar cualquiera de las clases Prepared Statement, asociada a cada uno de los motores de bases de datos soportados, consiste en preparar (y de ahí su nombre), la sentencia SQL que deseamos ejecutar. Por ejemplo, en nuestro caso se corresponderá con la siguiente línea teniendo en cuenta que estamos utilizando una base de datos SQLite:
dim ps as SQLitePreparedStatement = bbdd.Prepare("insert into album(title, artistid) values(?,'0')")
La parte interesante se encuentra a la derecha de la asignación, en el método Prepare que está disponible a través de nuestra propiedad declarada con el tipo SQLitedatabase. Como puedes observar, la información que se pasa al método Prepare es la instrucción SQL que deseamos ejecutar con una diferencia sobre la instrucción original o directa: en este caso se utiliza el caracter de cierre de interrogación (“?”) como un marcador de posición por cada una de las variables o datos que deseamos sanear previamente a su ejecución. Observa que no hacemos esto con el valor ‘0’ puesto que es una constante, no introducida por el usuario.
Es precisamente este marcador de posición el que permitirá tratar previamente la información que deseemos utilizar, antes de que llegue a ejecutarse en el motor de base de datos. De este modo, se detecta y evita el recurso que hemos visto previamente y que supone el punto de entrada de las inyecciones SQL.
Una vez definida o preparada la instrucción propiamente dicha, el siguiente paso consiste en indicar qué tipo de dato es el que se corresponde con cada uno de los marcadores de posición utilizados, para lo cual se utiliza el índice de posición que ocupa cada uno de ellos en la instrucción, comenzando por el índice 0 como primera posición:
ps.BindType(0, SQLitePreparedStatement.SQLITE_TEXT)
Así, mediante la anterior instrucción, estaremos indicando que el primero de los marcadores de posición es de tipo texto.
Por último tan sólo restará asociar el valor, variable o fuente de entrada con cada uno de los marcadores de posición utilizados, empleando nuevamente el índice de posición para indicar la posición asignada:
ps.Bind(0, TextField1.Text)
En este caso observa que estamos asociando al primer marcador de posición el texto obtenido en el control TextField1.
Finalmente, sólo restará ejecutar la sentencia SQL propiamente dicha. Algo tan simple como ejecutar la siguiente instrucción:
ps.SQLExecute
Recupera la base de datos de ejemplo para que cuente con todas sus tablas originales, ejecuta nuevamente la aplicación y vuelve a introducir la sentencia que, anteriormente, dio al traste con la tabla Customers. En esta ocasión comprobarás, sin embargo, que el Prepared Statement ha hecho su trabajo atajando el problema.
Conclusiones
Has comprobado cuan sencillo resulta exponer la integridad de las bases de datos en nuestras aplicaciones, frente a las inyecciones SQL, cuando no ponemos los remedios para sanea previamente los datos de entrada. Sin embargo, el framework de Xojo lo pone realmente fácil incluyendo simplemente unas pocas líneas más de código en nuestras aplicaciones.
[…] proporciona a los desarrolladores soporte de declaraciones preparadas para el acceso a bases de datos. Con ello se toman los valores que se van a usar en la consulta y […]