Ver índice de contenidos del libro

19.2. Inyección de SQL

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étodo extra() (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.