La inyección de SQL es un exploit común en el cual un atacante altera los
parámetros de la página (tales como datos de GET
/POST
o URLs) para
insertar fragmentos arbitrarios de SQL que una aplicación Web ingenua ejecuta
directamente en su base de datos. Es probablemente la más peligrosa — y,
desafortunadamente una de las más comunes — vulnerabilidad existente.
Esta vulnerabilidad se presenta más comúnmente cuando se está construyendo SQL "a mano" a partir de datos ingresados por el usuario. Por ejemplo, imaginemos que se escribe una función para obtener una lista de información de contacto desde una página de búsqueda. Para prevenir que los spammers lean todas las direcciones de email en nuestro sistema, vamos a exigir al usuario que escriba el nombre de usuario del cual quiere conocer sus datos antes de proveerle la dirección de email respectiva:
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = '%s';" % username
# execute the SQL here...
Nota En este ejemplo, y en todos los ejemplos similares del tipo "no hagas esto" que siguen, hemos omitido deliberadamente la mayor parte del código necesario para hacer que el mismo realmente funcione. No queremos que este código sirva si accidentalmente alguien lo toma fuera de contexto y lo usa.
A pesar de que a primera vista eso no parece peligroso, realmente lo es.
Primero, nuestro intento de proteger nuestra lista de emails completa va a
fallar con una consulta construida en forma ingeniosa. Pensemos acerca de qué
sucede si un atacante escribe "'OR 'a'='a"
en la caja de búsqueda. En ese
caso, la consulta que la interpolación construirá será:
SELECT * FROM user_contacts WHERE username = '' OR 'a' = 'a';
Debido a que hemos permitido SQL sin protección en la string, la cláusula
OR
agregada por el atacante logra que se retornen todas los registros.
Sin embargo, ese es el menos pavoroso de los ataques. Imaginemos qué
sucedería si el atacante envía "'; DELETE FROM user_contacts WHERE 'a' = 'a'"
. Nos encontraríamos con la siguiente consulta completa:
SELECT * FROM user_contacts WHERE username = ''; DELETE FROM user_contacts WHERE 'a' = 'a';
¡Ouch! ¿Donde iría a parar nuestra lista de contactos?
19.2.1. La solución
Aunque este problema es insidioso y a veces difícil de detectar la solución es simple: nunca confíes en datos provistos por el usuario y siempre escapa el mismo cuando lo conviertes en SQL.
La API de base de datos de Django hace esto por ti. Escapa automáticamente todos los parámetros especiales SQL, de acuerdo a las convenciones de uso de comillas del servidor de base de datos que estés usando (por ejemplo, PostgreSQL o MySQL).
Por ejemplo, en esta llamada a la API:
foo.get_list(bar__exact="' OR 1=1")
Django escapará la entrada apropiadamente, resultando en una sentencia como esta:
SELECT * FROM foos WHERE bar = '\' OR 1=1'
que es completamente inocua.
Esto se aplica a la totalidad de la API de base de datos de Django, con un par de excepciones:
- El argumento
where
del métodoextra()
(ver Apéndice C). Dicho parámetro acepta, por diseño, SQL crudo. - Consultas realizadas "a mano" usando la API de base de datos de nivel más bajo.
En tales casos, es fácil mantenerse protegido. para ello evita realizar interpolación de strings y en cambio usa parámetros asociados (bind parameters). Esto es, el ejemplo con el que comenzamos esta sección debe ser escrito de la siguiente manera:
from django.db import connection
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s;"
cursor = connection.cursor()
cursor.execute(sql, [user])
# ... do something with the results
El método de bajo nivel execute
toma un string SQL con marcadores de
posición %s
y automáticamente escapa e inserta parámetros desde la lista
que se le provee como segundo argumento. Cuando construyas SQL en forma manual
hazlo siempre de esta manera.
Desafortunadamente, no puedes usar parámetros asociados en todas partes en
SQL; no son permitidos como identificadores (esto es, nombres de tablas o
columnas). Así que, si, por ejemplo, necesitas construir dinámicamente una
lista de tablas a partir de una variable enviada mediante POST
, necesitarás
escapar ese nombre en tu código. Django provee una función,
django.db.backend.quote_name
, la cual escapará el identificador de acuerdo
al esquema de uso de comillas de la base de datos actual.