¿Cómo funciona realmente la inyección SQL, y cómo la detienen las consultas parametrizadas?
La inyección SQL ocurre en el momento en que una entrada no confiable puede cambiar la forma de una consulta, no solo sus valores — las consultas parametrizadas lo evitan por construcción.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funciona realmente la inyección SQL, y cómo la detienen las consultas parametrizadas?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#El mecanismo central: texto de la consulta vs. datos de la consulta
Una consulta SQL tiene dos tipos de contenido fundamentalmente distintos: la gramática (SELECT, WHERE, AND, nombres de tablas y columnas) y los datos (los valores concretos que se comparan o insertan). La inyección SQL ocurre en el instante en que ambos se mezclan en la misma cadena antes de que la base de datos los vea. Si un valor que viene de la entrada del usuario puede cambiar dónde termina una cláusula, agregar una cláusula nueva o comentar parte de la consulta, el atacante ya no está simplemente aportando datos — está coescribiendo el SQL que ejecuta tu aplicación.
#El patrón vulnerable: concatenación de strings
# VULNERABLE — la entrada del usuario se concatena directamente en el texto SQLdef find_user(cursor, username, password):query = f"""SELECT id, username, is_adminFROM usersWHERE username = '{username}' AND password_hash = '{password}'"""cursor.execute(query)return cursor.fetchone()
Esto se ve completamente razonable hasta que username contiene una comilla simple. Envía username = "admin' --" con cualquier contraseña, y la consulta que la base de datos realmente ejecuta es:
SELECT id, username, is_adminFROM usersWHERE username = 'admin' --' AND password_hash = 'anything'
-- inicia un comentario SQL, así que todo lo que sigue — incluida la comprobación de la contraseña — se descarta antes de que la base de datos siquiera lo parsee. El atacante queda autenticado como admin sin haber sabido nunca la contraseña. Un payload distinto, ' OR '1'='1, convierte la cláusula WHERE en una tautología que coincide con todas las filas, lo cual es igual de peligroso contra una consulta que ni siquiera fuera de login (exfiltración masiva de datos desde cualquier endpoint que filtre por texto proporcionado por el usuario).
#La solución: consultas parametrizadas
# CORREGIDO — la forma de la consulta es fija; la entrada viaja como dato, nunca como texto SQLdef find_user(cursor, username, password_hash):query = """SELECT id, username, is_adminFROM usersWHERE username = %s AND password_hash = %s"""cursor.execute(query, (username, password_hash))return cursor.fetchone()
El mecanismo aquí no es "el driver escapa mejor las comillas" — es estructuralmente distinto. El driver de la base de datos envía la plantilla de la consulta y los valores de los parámetros a la base de datos como dos piezas separadas por el protocolo de red. La base de datos compila la plantilla — decidiendo cuál es la gramática — antes de ver los valores, y luego enlaza los valores al plan ya fijado puramente como datos. Un valor como admin' -- nunca puede convertirse en parte de la gramática, sin importar qué caracteres contenga, porque para cuando la base de datos lo ve, la gramática ya fue decidida.
#Lo que la parametrización no cubre
Los placeholders funcionan para valores — cosas que van donde iría un literal. No funcionan para identificadores — nombres de tabla, nombres de columna, la dirección de un ORDER BY (ASC/DESC) — porque esos forman parte de la gramática misma, no de un valor que se compara. Si necesitas construir una consulta con un nombre de columna o tabla dinámico, tienes que validarlo contra una lista fija de nombres conocidos y seguros en tu propio código; no hay forma de parametrizar la salida de ese caso.
#Más allá del caso clásico
| Variante | Cómo se manifiesta | Por qué las consultas parametrizadas la detienen igual |
|---|---|---|
| Clásica / in-band | Una cláusula inyectada cambia los resultados o evita la autenticación (' OR '1'='1) | La entrada se enlaza como un valor literal y nunca puede formar parte de la gramática |
| Blind boolean-based | El atacante infiere datos bit a bit por diferencias verdadero/falso en el comportamiento de la app, sin necesitar un error visible | La misma solución — el payload nunca llega al parser como SQL de todas formas |
| Blind time-based | El atacante inyecta algo como ... OR SLEEP(5) y mide la latencia de la respuesta | La misma solución — SLEEP(5) se queda como un literal de texto inofensivo, nunca se ejecuta como SQL |
| Inyección de segundo orden | Una entrada maliciosa se guarda de forma segura, y luego se lee y se concatena de forma insegura en una consulta distinta | Hay que parametrizar todas las consultas que tocan ese dato, no solo la que lo escribió primero |
🧠 Check your understanding
0/3 · 0/3 answered1. Una consulta de login se construye con un f-string de Python: WHERE username = '{username}' AND password = '{password}'. Un atacante envía username = admin' --. ¿Qué ocurre?
2. ¿Por qué las consultas parametrizadas evitan la inyección SQL de forma estructural, y no solo 'escapando mejor los caracteres peligrosos'?
3. ¿Por qué no se pueden pasar nombres de tabla o columna a través de un placeholder parametrizado?