Un buscador que muestra un error
Una tienda pequeña tiene un buscador para su lista de productos. Un visitante escribe una palabra con apóstrofo, como D'Angelo.
La página responde con una línea de palabras raras del servidor. El dueño dice que es un fallo menor. En realidad es una señal: la página deja que el visitante escriba parte de sus preguntas.
La regla única
Advertencia
Este laboratorio es una tienda insegura a propósito. Ataca solo tu copia privada, que no tiene salida a internet. Atacar un sistema que no es tuyo, o que no tienes permiso para probar, es delito en casi todos los países.
Al final de esta lección
Nota
Puedo corregir una consulta para que el texto escrito no pueda cambiarla.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Base de datos | almacén ordenado de registros al que un programa hace preguntas | |
| Consulta | pregunta que un programa hace a una base de datos | SELECT name FROM products |
| Inyección SQL | texto escrito que cambia la consulta que envía un programa | |
| Parámetro | valor que se envía junto a la consulta y nunca dentro de ella | ? |
| Cadena | trozo de texto dentro de un programa | "pen" |
| Entrada | todo lo que un visitante escribe o envía a una página |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una consulta hecha con texto se puede cambiar
Muchos programas montan una consulta pegando un principio fijo y lo que el visitante escribió. La base de datos recibe una sola cadena larga. No sabe qué parte puso quien programó la página y cuál el visitante.
Si el texto del visitante lleva una comilla, puede cerrar antes de tiempo el texto fijo del programa. Lo que viene detrás se lee como parte de la consulta.
La consulta que monta el programa
# Cambia term por "Cuaderno" y pulsa Run. Luego prueba "Baker's".
term = "pen"
start = "SELECT name, price FROM products WHERE name LIKE '%"
query = start + term + "%'"
print(query)
Ejemplo resuelto: tres búsquedas
| El visitante escribe | Consulta que recibe la base | Qué ocurre |
|---|---|---|
| pen | ... LIKE '%pen%' | Funciona como se pensó |
| Baker's | ... LIKE '%Baker's%' | La comilla cierra el texto antes de tiempo y la consulta se rompe |
| x' AND 1=2 -- | ... LIKE '%x' AND 1=2 --%' | El visitante añadió una condición propia |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Mira la tienda de tu laboratorio
Tu laboratorio tiene una tienda real con esta debilidad. Arranca el laboratorio y ejecuta estos tres comandos. Solo miran. El primero es una búsqueda normal. El segundo usa un nombre con apóstrofo, que la tienda no sabe manejar. El tercero imprime la línea de código que monta la consulta.
Una búsqueda normal
curl -s -G --data-urlencode "q=pen" http://app:8000/searchSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un nombre con apóstrofo
curl -s -G --data-urlencode "q=Baker's" http://app:8000/searchSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
La línea que monta la consulta
docker exec app grep -n "rows = db.execute" /tmp/app/app.pySe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Qué te dicen las tres respuestas
El primero imprime Pen 1.2, el nombre y el precio. El segundo imprime una línea que empieza por database error. La tienda metió tu apóstrofo tal cual en su consulta.
El tercero imprime un número de línea y una línea larga de código. Busca los signos +. Cada uno pega texto escrito dentro de la consulta.
Un parámetro deja el texto fuera
Un parámetro lo arregla. El programa envía la consulta con un ? donde va el valor. El texto escrito viaja aparte.
La base de datos lee primero la consulta y fija su forma. El texto llega después y solo puede ser un valor. Ninguna comilla puede cerrar nada.
La misma búsqueda con parámetro
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE products (name TEXT, price REAL)")
db.execute("INSERT INTO products VALUES (?, ?)", ("Baker's apron", 14.0))
db.execute("INSERT INTO products VALUES (?, ?)", ("Pen", 1.2))
# Cambia term y pulsa Run. Nada de lo que escribas cierra la consulta.
term = "Baker's"
rows = db.execute("SELECT name, price FROM products WHERE name LIKE ?", ("%" + term + "%",)).fetchall()
print(rows)
Un error frecuente
Advertencia
Mucha gente cree que basta con quitar el apóstrofo del texto. No es así. Los filtros se dejan casos, y un caso olvidado es una puerta abierta. El parámetro funciona porque nunca mezcla el texto con la consulta, por ingenioso que sea. Úsalo para cada valor.
Hay partes que no pueden ser parámetros
Un parámetro puede ser un valor. No puede ser el nombre de una columna ni un orden. Una página que deja ordenar por precio o por nombre tiene que escribir esa palabra en la consulta.
La forma segura es una lista corta de palabras permitidas. Si la elección del visitante no está en la lista, se usa una por defecto.
Solo palabras de la lista
ALLOWED = ["name", "price"]
# Cambia choice a "price" y luego a "stock; drop", y pulsa Run.
choice = "name"
if choice in ALLOWED:
column = choice
else:
column = "name"
print("ORDER BY " + column)