Inyección SQL con Python
Table of Contents
ToggleIntroducción
La inyección de código SQL es un tipo de vulnerabilidad informática que permite a un usuario no autorizado ejecutar consultas SQL en una base de datos. Esto puede ocurrir en cualquier tipo de aplicación (sitios web, aplicación de escritorio, móvil, etc). Dicho fallo de seguridad ocurre por la falta de sanitización o seguridad en el desarrollo de programas, ya que, debido a esto, un usuario puede ingresar consultas SQL maliciosas y obtener acceso a la base de datos, y peor aún, al sistema. Por lo tanto, en este apartado estaremos viendo la forma en la que ocurre la inyección SQL con Pythonesta, y cómo prevenirlas con los módulos SQLite y MySQL.
Si desea saber cómo interactuar con los módulos que veremos a continuación, puede visitar nuestro artículo de base de datos con Python. También, puede ver en el nivel avanzado las distintas formas y consultas para aplicar inyección SQL.
Prevención con SQLite3
Para entender mejor esto, vamos a generar un programa que nos pida ingresar el nombre de usuario y edad.
import sqlite3
conn = sqlite3.connect(«empresa.sqlite»)
cursor = conn.cursor()
try:
cursor.execute(«CREATE TABLE empleados (nombre TEXT, edad NUMERIC)»)
except sqlite3.OperationalError:
pass
nombre = input(«Nombre: «)
edad = int(input(«Edad: «))
cursor.execute(f»INSERT INTO empleados VALUES (‘{nombre}’, {edad})»)
conn.commit()
print(«¡Los datos han sido ingresados correctamente!»)
conn.close()
Como puede observar, hemos especificado la BD “empresa”, la tabla “empleados” y los campos “Nombre” y “Edad”. Cuyo valor a ingresar, se vería de la siguiente forma:
Nombre: Pablo
Edad: 27
¡Los datos han sido ingresados correctamente!
Teniendo esto en cuenta, recordemos que los datos ingresados por el usuario se ejecutan a través de la sintaxis de consulta que hemos generado anteriormente, mas precisamente la siguiente:
cursor.execute(f»INSERT INTO empleados VALUES (‘{nombre}’, {edad})»)
Aplicación con SQLite3
Teniendo en cuenta lo anterior, la consulta que se ejecuta por detrás sería la siguiente:
INSERT INTO empleados VALUES (‘Pablo’, 27)
Aquí, el problema radica en que no hay validación, o sanitización alguna, con respecto a los datos ingresados por parte del usuario. Entonces, un usuario con malas intenciones podría colocar la siguiente sintaxis:
Nombre: Pablo‘, 27); DELETE FROM empleaos; —
Edad: 27
Siguiendo la lógica de sintaxis en nuestro programa, esto se vería de la siguiente forma:
INSERT INTO empleados VALUES (‘Pablo‘, 27); DELETE FROM empleaos; —’, 27)
Pasos a seguir
1) Primero, estamos insertando el nombre (Pablo) y su edad (27) en la misma sintaxis.
2) Segundo, si prestamos atención, podemos ver que después del nombre hemos colocado una comilla simple ‘ , luego en la edad, hemos cerrado con un paréntesis ) y, finalmente, con punto y coma ;
Los caracteres marcados con rojo cumplen la siguiente función:
La comilla simple se usa para anteponerse de la comilla principal(‘Pablo’), la cual termina quedando al final de la consulta(‘, 27).
El paréntesis lo utilizamos para marcar y cerrar el final de la variable, en este caso, el valor de edad ,27).
Por último, el punto y coma lo usamos para separar una consulta de otra, y así, ejecutar más de una en una misma línea.
3) Tercero, hemos colocado la consulta para borrar todas las filas de la tabla empleados (DELETE FROM empleados), usando al final el doble guión – – para comentar el resto de la consulta que viene por defecto en el programa, generando así, que solo se ejecute el código malicioso.
Por lo tanto, hasta ahora, teóricamente habríamos logrado inyectar una consulta SQL a partir de la función input(). Pero si ingresamos estos datos de nuevo (es decir, el nombre y la edad tal como lo indicamos más arriba) veremos que el programa arroja el siguiente error.
Traceback (most recent call last):
[…]
cursor.execute(f»INSERT INTO personas
VALUES (‘{nombre}’, {edad})»)
sqlite3.Warning: You can only execute one
statement at a time.
Podemos notar, para nuestra sorpresa, que el módulo execute() de SQLite3 solo nos deja ejecutar una sola línea de código, de modo que toda cadena que le pasemos como argumento y que posea más de una consulta (separados por punto y coma) hará que módulo sqlite3 nos devuelva dicho error.
Función executescript()
Teniendo en cuenta el problema anterior (“You can only execute one statement at a time”) con sqlite3, debemos tener en cuenta que esto ocurre particularmente con este módulo, y puede NO aplicarse para otros módulos para interactuar con base de datos. Para estos casos podemos usar la función executescript(). Entonces, habiendo visto la consulta anterior, quedaría de la siguiente manera:
cursor.executescript(f»INSERT INTO empleados VALUES (‘{nombre}’, {edad})»)
Ahora bien, volvamos a ingresar los valores anteriores para ejecutar el programa:
Nombre: Pablo‘, 27); DELETE FROM empleaos; —
Edad: 27
Veremos que no obtenemos ningún tipo de error como resultado y, que al intentar volver a ingresar a la tabla empleados, nos daremos cuenta estará vacía. Por lo tanto ¡Nuestra inyección SQL habrá sido exitosa!
¿Cómo prevenimos esta vulnerabilidad?
Recordemos que la inyección SQL, ya sea con Python o en cualquier sistema, radica en la posibilidad de un usuario poder ejecutar código a través de una entrada. Por lo tanto, no debería haber comillas de ningún tipo en las variables o caracteres que permitan esto. Por ejemplo, las siguientes cadenas SI generan esta vulnerabilidad:
- execute(«INSERT INTO empleados VALUES (‘» + nombre + «‘,» + str(edad) + «)»)
- execute(«INSERT INTO empleados VALUES (‘%s’, %d)» %(nombre, edad))
- execute(f»INSERT INTO empleados VALUES (‘{nombre}’,{edad})»)
- execute(«INSERT INTO empleados VALUES (‘{}’,{})».format(nombre, edad))
La única forma de prevenirla es chequear que los valores de las variables (en este caso “nombre” y “edad”) no contengan código SQL. Recordemos que la función execute() lo hará por nosotros (por lo que no hace falta hacerlo manualmente) si pasamos esos datos en una tupla como segundo argumento y, en la consulta, utilizamos un signo de interrogación para indicar los lugares en donde deben ubicarse dichos datos.
cursor.execute(«INSERT INTO personas VALUES (?, ?)», (nombre, edad))
Como se mencionó en el tópico anterior. Podemos observar que, por más que la variable nombre sea una cadena, dentro de la consulta NO usamos comillas alrededor del signo de interrogación. De esta forma, hemos eliminado la posibilidad de inyección de código SQL en nuestra aplicación.
Prevención con PyMysql
Habiendo visto la inyección SQL con Python anteriormente, debemos tener en cuenta que en cualquier lenguaje de programación, motor de base de datos o módulos para interactuar con ellos, siempre es la misma. Es decir, siempre se busca evitar la inclusión de variables por parte de los usuarios, dejando así, que el módulo se encargue de ello. Sin embargo, pueden haber algunos cambios o diferencia con respecto al módulo que se esté usando. Ahora nos enfocaremos en el motor de base de datos MySQL, más precisamente en el módulo pymysql.
Aplicación con Pymysql
Considerando el programa en el módulo anterior. Con pymysql se vería de la siguiente forma:
import pymysql
conn = pymysql.connect(
host=»localhost»,
user=»username»,
passwd=»password»,
db=»nombre_db»
)
cursor = conn.cursor()
try:
cursor.execute(«CREATE TABLE empleados (nombre VARCHAR(50), edad INT)»)
except pymysql.err.InternalError:
pass
nombre = input(«Nombre: «)
edad = int(input(«Edad: «))
cursor.execute(f»INSERT INTO empleados VALUES (‘{nombre}’, {edad})»)
conn.commit()
print(«¡Los datos han sido ingresados correctamente!»)
conn.close()
Parámetro cliente_flag
Hasta aquí ya podríamos probar inyectar código SQL con python a través de la consulta mencionada anteriormente. Sin embargo, el módulo pymysql, al igual que sqlite3, no admite por defecto la ejecución de múltiples consultas en una misma llamada a connect(). Para ello, hacemos uso del siguiente parámetro:
client_flag=pymysql.constants.CLIENT.MULTI_STATEMENTS
Pasando esto a la función connect(), nos quedaría de la siguiente forma:
conn = pymysql.connect(
host=»localhost»,
user=»username»,
passwd=»password»,
db=»nombre_db»,
client_flag=pymysql.constants.CLIENT.MULTI_STATEMENTS
)
Por tanto, la inyección SQL con python habrá sido satisfactoria y se habrán eliminado todas las filas de la tabla empleados.
Ahora bien, a diferencia de sqlite3, con el módulo pymysql estaremos usando los caracteres %s en lugar de los signos de interrogación. Por lo tanto, esto quedaría de la siguiente manera:
cursor.execute(«INSERT INTO empleados VALUES (%s,%s)», (nombre, edad))
No olvidar
La siguiente consulta SI genera la inyección de SQL:
cursor.execute(«INSERT INTO empleados VALUES (‘%s’, %d)» % (nombre, edad))
El operador %d es utilizado para representar la POSICIÓN de un caracter con respecto a una consulta, mientras que %s representa al caracter en si.