flawopen.com/Inyección SQL/Python

Inyección SQL en Python

Crítica CWE-89 Borrador — pendiente de revisión
Language: English Deutsch Español Français हिन्दी Bahasa Indonesia 日本語 한국어 Português (Brasil) Русский 简体中文
Explicación sencilla

Imagina un formulario que solo espera un número de ticket, como 482. La inyección SQL ocurre cuando alguien escribe algo malicioso en esa casilla en lugar de un número — una frase trampa que hace que el sistema responda "muéstrame todos los tickets" en lugar de solo el ticket 482, porque el sistema nunca verificó que lo que recibió fuera realmente solo un número.

Términos clave de esta página
entrada controlada por el usuario
Cualquier valor que, en última instancia, provenga de quien usa — o ataca — la aplicación: un campo de formulario, un parámetro de URL, el nombre de un archivo subido, una cabecera HTTP. La aplicación no puede asumir que es válido o seguro.
consulta SQL
El comando enviado a una base de datos — por ejemplo, "obtener esta fila", "eliminar esta tabla". Su significado depende por completo de su texto exacto, lo que es justo lo que hace peligroso inyectar texto adicional en él.

Qué está pasando

La inyección SQL ocurre cuando una entrada controlada por el usuario se inserta directamente en el texto de una consulta a la base de datos, en lugar de pasarse como un valor independiente. Si el código construye una consulta uniendo cadenas de texto, un atacante puede proporcionar una entrada que cambie la estructura real de la consulta — convirtiendo una consulta de "buscar una fila" en una que devuelve todas las filas, o que elimina una tabla.

En Python, esto casi siempre aparece de la misma forma: una llamada a la base de datos construida con un f-string, formato con %, o concatenación con +, en lugar de los marcadores de parámetros integrados del driver de la base de datos.

Impacto en el mundo real

En 2015, la operadora británica TalkTalk sufrió una brecha que afectó a más de 150.000 clientes después de que unos atacantes explotaran una falla de inyección SQL en una página web heredada de una adquisición. El organismo regulador de protección de datos del Reino Unido multó a TalkTalk con £400.000, describiendo el fallo como evitable y básico.

Fuente: notificación de sanción de la Information Commissioner's Office del Reino Unido, 2016 — ver Referencias más abajo.

Vulnerable vs. corregido

VULNERABLE
# user_id viene directamente de la petición
def get_user(cursor, user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
    cursor.execute(query)
    return cursor.fetchone()
CORREGIDO
# el valor se pasa por separado, nunca insertado
def get_user(cursor, user_id):
    query = "SELECT * FROM users WHERE id = %s"
    cursor.execute(query, (user_id,))
    return cursor.fetchone()

Por qué funciona la corrección

La versión corregida pasa el texto de la consulta y el valor a execute() como dos argumentos separados. El driver de la base de datos también los envía por separado a la base de datos — la estructura de la consulta queda fijada antes de que el valor se le adjunte, así que el valor nunca puede interpretarse como parte de la sintaxis SQL, sin importar qué caracteres contenga. Un f-string no puede lograr esto, porque, para cuando execute() recibe la consulta, el valor ya se incorporó al texto como si siempre hubiera formado parte del comando.

Particularidades de Python

El formato con % dentro de execute() todavía parece parametrizado — pero no lo es

cursor.execute("... WHERE id = %s" % user_id) es tan vulnerable como un f-string. El marcador solo se vuelve seguro cuando el valor se pasa como el segundo argumento de execute() en sícursor.execute("...WHERE id = %s", (user_id,)) — para que sea el driver, y no el formateo de cadenas de Python, quien haga la sustitución.

Los ORM parametrizan por defecto, pero sus vías de escape no

El ORM de Django y el constructor de consultas de SQLAlchemy parametrizan automáticamente en las consultas normales. El riesgo vuelve en cuanto usas Model.objects.raw() o text() de SQLAlchemy y construyes ese SQL crudo con un f-string.

La sintaxis de los marcadores no es uniforme entre drivers

psycopg2 (PostgreSQL) usa %s sin importar el tipo de columna; sqlite3 usa ?. Copiar el estilo de marcador de la documentación de un driver a otro falla de forma silenciosa — comprueba el estilo de parámetros de tu driver concreto en lugar de asumirlo.

Conceptos erróneos comunes

"Mi ORM me protege automáticamente"

Cierto para la API de consulta normal del ORM — falso en cuanto usas raw() o text() y construyes esa cadena tú mismo.

"Este ID siempre es un número, así que es seguro interpolarlo"

El riesgo no está en el tipo del valor en tiempo de ejecución — está en que la consulta se construye por interpolación de cadenas. En cuanto esa suposición falle en cualquier momento del ciclo de vida del código, la vulnerabilidad ya estará ahí esperando.

"Yo mismo escapo las comillas, así que no necesito consultas parametrizadas"

El escapado manual depende del driver y es fácil de hacer mal de forma sutil. Las consultas parametrizadas no son una versión más estricta del escapado — evitan el problema por completo, porque el valor nunca forma parte del texto de la consulta.

Cómo comprobar si te afecta

grep -rn "execute(f\"" --include="*.py" . grep -rn "execute(.*%\s*(" --include="*.py" . grep -rn "\.raw(\|text(" --include="*.py" .
Mejor que solo grep: ejecuta Bandit (regla B608, hardcoded_sql_expressions) en el CI — detecta este patrón automáticamente y falla el build ante una nueva aparición.

Checklist de prevención

Preguntas frecuentes

¿Usar un ORM me protege de la inyección SQL?

Para sus métodos de consulta normales, sí. Sus vías de escape para consultas crudas no — son exactamente tan seguras como SQL escrito a mano, ni más.

¿Este riesgo solo existe en cosas como los buscadores?

No. Cualquier cosa efectivamente controlada por un tercero cuenta — cabeceras HTTP, nombres de archivos subidos, incluso un valor proveniente de una API externa en la que confía tu aplicación.

¿Puedo simplemente escapar las comillas yo mismo?

Puedes, pero es frágil y depende del driver. Las consultas parametrizadas son la corrección real, no una versión más estricta del escapado.

Referencias

Ver en: Python JavaScriptGoJava PHPC#Ruby C/C++RustKotlin Swift Solidity (N/A)
Ver también: Inyección de ComandosPath Traversal XSSDeserialización Insegura