Demuestra que un endpoint es vulnerable a la inyección con un comportamiento repetible y específico de la base de datos en un entorno de pruebas autorizado. Por eso, vincula cada valor en el servidor, incluye los identificadores en una allow-list y restringe la cuenta de la base de datos.
Para GET /product?id=1, la aplicación debería enviar una consulta fija y vincular 1 como dato. Esa regla evita el fallo principal con los valores. Por eso, esta guía cubre lo que no puede vincular y los controles de daños que siguen siendo necesarios.
Las listas de payloads son un mal punto de partida. La mayoría de los procesos de triaje de SQL injection salen mal porque las personas clasifican payloads en lugar de evidencias. Así que identificaremos el comportamiento observable y lo probaremos contra un objetivo de staging preparado. Después corregiremos las rutas de consulta que lo permitieron.
La inyección SQL se ha mantenido cerca de los primeros puestos de las advertencias de seguridad de aplicaciones por una razón. OWASP la clasificó en primer lugar en los Top 10 de 2013 y 2017; A03:2021 Injection ocupó el tercer puesto. Invicti informa que CVE-2025-1094, un problema de SQL injection de PostgreSQL, apareció en una cadena de ataque que alcanzó infraestructura del Tesoro de Estados Unidos en enero de 2025. Esa atribución corresponde a Invicti; no es una razón para convertir esta guía en un resumen de brechas.
En este artículo
- La inyección SQL es un fallo del límite entre código y datos
- Clasifica SQLi según cuándo se ejecuta y cómo la observas
- La SQLi in-band da la respuesta en la respuesta normal
- La SQLi blind convierte las diferencias de respuesta en una medición
- Prueba únicamente endpoints que poseas y demuestra menos de lo que puedes
- Las consultas parametrizadas deberían ser el valor predeterminado en todas partes
- El SQL dinámico necesita allow-lists, no escapes ingeniosos
- Usa una checklist de lanzamiento que cubra el código y los daños
La inyección SQL es un fallo del límite entre código y datos
Un endpoint vulnerable podría convertir esta solicitud:
GET /product?id=1
en:
SELECT name, price FROM products WHERE id = '1'
Si la aplicación concatena la entrada, la sintaxis SQL de la solicitud puede cambiar la estructura de la consulta. Por eso, la base de datos recibe instrucciones de la aplicación y texto no confiable en un mismo comando.
La forma segura es:
SELECT name, price FROM products WHERE id = ?
El driver vincula 1 por separado. Por eso, un valor como tom' or '1'='1 permanece como un nombre de usuario literal en lugar de convertirse en una nueva condición. La SQL Injection Prevention Cheat Sheet de OWASP establece la regla que rige: las sentencias preparadas impiden que un atacante cambie la intención de una consulta.
Ten presentes tres decisiones. Vincula los valores mediante una API de base de datos del lado del servidor. Asigna los identificadores dinámicos a fragmentos SQL fijos. La aplicación solo necesita los permisos que utiliza.
Si tu solución es “escapar la comilla”, no has corregido la inyección SQL; has elegido una forma más frágil de seguir escribiendo SQL.
Clasifica SQLi según cuándo se ejecuta y cómo la observas
Clasifica el fallo en dos ejes: cuándo la entrada se vuelve peligrosa y cómo observas el resultado.
- La SQLi clásica, o in-band, devuelve resultados de la base de datos o errores de la base de datos en la respuesta HTTP normal.
- La SQLi blind oculta el resultado, por lo que infieres una condición a partir de una página, un estado, una longitud de respuesta, un error o un tiempo de respuesta modificados.
- La SQLi out-of-band hace que la base de datos realice una solicitud externa, normalmente DNS, que observas por separado.
- La SQLi de segundo orden almacena la entrada durante una operación y después la reutiliza en una consulta vulnerable. Describe el momento de ejecución, por lo que se solapa con las categorías de feedback anteriores.
Para /product?id=1, inspecciona qué cambia cuando cambia la entrada. ¿La respuesta expone filas adicionales o un error? ¿Una condición verdadera produce un cuerpo diferente al de una condición falsa? Las solicitudes que de otro modo serían equivalentes también podrían mostrar un retraso controlado.
Una respuesta modificada es un indicador. La confirmación requiere repetibilidad, además de una explicación específica de la base de datos o respaldada por telemetría, idealmente reproducida en un laboratorio controlado.
Esa distinción es importante. Un scanner puede detectar una diferencia sospechosa sin demostrar que la sintaxis SQL llegó a la base de datos.
La SQLi in-band da la respuesta en la respuesta normal
La SQLi clásica, o in-band, es la clase más fácil de demostrar y la más fácil de probar en exceso. El feedback útil llega en la respuesta HTTP ordinaria, ya sea como resultados modificados o como un error de la base de datos.
El ejemplo de la cookie de seguimiento de PortSwigger utiliza una respuesta que contiene un mensaje “Welcome back” para mostrar si una condición modificó la consulta subyacente. En tu propio laboratorio, una prueba controlada podría hacer que un endpoint devuelva una fila sintética adicional preparada deliberadamente. La prueba es la relación repetible entre la entrada y el resultado, no la cantidad de filas que puedas recuperar.
El comportamiento basado en errores es otro canal de respuesta. Una aplicación podría devolver:
Unterminated string literal started at position 52 in SQL SELECT * FROM tracking WHERE id = '''. Expected char
Ese mensaje revela la forma de la consulta y el comportamiento de la base de datos. Un laboratorio preparado deliberadamente también puede demostrar errores de conversión con:
CAST((SELECT example_column FROM example_table) AS int)
No adaptes esto a credenciales reales ni a datos de producción. Mantén la consulta dentro de un entorno desechable que contenga valores creados específicamente para la prueba. Algunas configuraciones de bases de datos y errores muestran el valor convertido; muchas configuraciones de producción lo suprimen.
Devuelve a los usuarios un identificador de error genérico. Conserva los diagnósticos detallados en logs protegidos y evita registrar valores sensibles de las solicitudes o texto de consulta innecesario. Ocultar el error elimina la evidencia; no repara la consulta.
La SQLi blind convierte las diferencias de respuesta en una medición
Cuando la respuesta oculta los resultados de la consulta, prueba si una condición controlada produce una señal repetible. En un entorno de staging autorizado con un marcador preparado deliberadamente, compara condiciones verdaderas y falsas apropiadas para la base de datos. El patrón ilustrativo de PortSwigger es:
xyz' AND '1'='1
xyz' AND '1'='2
La sintaxis exacta depende del contexto de la consulta y de la base de datos. No pegues esto en sistemas que no te pertenezcan.
Una condición SUBSTRING puede probar una posición de carácter, mientras que una comparación como > 'm' reduce un marcador sintético conocido mediante búsqueda binaria. Utiliza esto únicamente para establecer el control sobre el valor preparado. Detente una vez que el marcador demuestre que la condición llega a la base de datos.
| Técnica | Señal | Detente cuando |
|---|---|---|
| Basada en booleanos | Una diferencia estable entre condiciones verdaderas y falsas | La diferencia se repita y la telemetría respalde la causa |
| Basada en errores | Un error condicional de la base de datos | Se haya establecido la correlación del error en el laboratorio |
| Basada en tiempo | Una diferencia controlada de latencia | El tiempo se diferencie de la línea base y la telemetría lo respalde |
| Out-of-band | Una interacción externa identificada de forma única | La callback se correlacione con la ruta de prueba |
Los errores condicionales pueden utilizar una expresión CASE:
CASE WHEN <condition> THEN 1/0 ELSE 'a' END
Esa señal desaparece cuando la aplicación gestiona todos los errores de la base de datos de forma idéntica.
Las pruebas de tiempo necesitan una línea base. Mide primero las solicitudes ordinarias, registra su distribución y después utiliza un retraso corto y controlado, repitiendo la comparación lo mínimo posible.
| Base de datos | Sintaxis del retraso |
|---|---|
| Microsoft SQL Server | WAITFOR DELAY |
| MySQL | SLEEP() |
| PostgreSQL | pg_sleep() |
La sintaxis es específica de la base de datos. La carga de red, el almacenamiento en caché, el estado de la sesión y la contención de la base de datos también pueden cambiar la latencia. No puedo afirmar que una diferencia de latencia sea SQLi basándome únicamente en respuestas HTTP; mídela contra una línea base y correlaciónala con la telemetría de la base de datos o de la aplicación.
Las pruebas de seguridad de aplicaciones out-of-band, a menudo llamadas OAST, ayudan cuando la respuesta es realmente poco informativa. La guía de PortSwigger sobre blind SQL injection describe la interacción externa como una señal útil en esa situación. Una callback DNS identificada de forma única puede confirmar una interacción externa, pero correlaciónala con la consulta controlada y la ruta de la aplicación antes de afirmar que el endpoint es vulnerable.
Prueba únicamente endpoints que poseas y demuestra menos de lo que puedes
Utiliza estas sondas únicamente contra sistemas que poseas o cuya prueba tengas autorización explícita para realizar. Prefiere un entorno local o de staging con registros sintéticos, una copia de seguridad reciente, límites de velocidad, monitorización y una regla de detención explícita.
Para /product?id=1, recopila primero varias solicitudes ordinarias. Después compara dos condiciones de prueba autorizadas y apropiadas para la base de datos contra el mismo registro preparado:
GET /product?id=<true-condition-for-the-staged-query>
GET /product?id=<false-condition-for-the-staged-query>
Los marcadores de posición anteriores son intencionados. La sintaxis correcta depende de cómo la aplicación entrecomille el valor y de qué base de datos lo ejecute. Genera el par en el laboratorio, nunca a partir de una lista genérica de payloads.
Compara el estado y la longitud del cuerpo. Comprueba también los marcadores estables y la latencia. Revisa por separado los logs de la base de datos y los IDs de correlación. Aquí corresponde incluir una advertencia sobre las pruebas: el estado de la caché, las sesiones, los reintentos y la carga del backend pueden producir diferencias de respuesta no relacionadas con la inyección SQL. Trata esas diferencias como indicadores hasta que una reproducción controlada o una explicación respaldada por telemetría confirme la causa.
Utiliza las herramientas para trabajos diferentes:
| Herramienta o categoría | Mejor uso | Limitación |
|---|---|---|
| Burp Suite u OWASP ZAP | Repetir solicitudes y probar manualmente | Requiere interpretación; los indicadores no son pruebas |
sqlmap | Confirmación específica en una prueba autorizada | Debe apuntarse a un punto de inyección; no sustituye el descubrimiento del flujo ni el análisis de segundo orden |
| DAST empresarial | Escaneado repetible en CI/CD | Menos flexible para la explotación personalizada |
| Scanners ligeros como Nikto, SQLiv o Wapiti | Reconocimiento amplio | Pueden producir falsos positivos |
| Scanners de plantillas como Nuclei | Descubrimiento a gran escala mediante plantillas | El descubrimiento no establece la explotabilidad |
Para un parámetro de staging designado, utiliza una detección conservadora:
sqlmap -u "https://staging.example.test/product?id=1" \
--batch --level=1 --risk=1
Esto evita deliberadamente --dbs; demostrar la inyección no requiere enumerar la base de datos. No conviertas una comprobación de lanzamiento en un ejercicio de extracción de datos.
Un hallazgo de un scanner es una pista, no un informe de brecha. Los flujos complejos de varios pasos y los casos de segundo orden siguen necesitando revisión de código, pruebas de integración y rastreo manual.
Las consultas parametrizadas deberían ser el valor predeterminado en todas partes
El orden de defensa de OWASP es correcto: sentencias preparadas, procedimientos almacenados implementados de forma segura, validación mediante allow-list para los valores que no se pueden vincular y escaping como recurso alternativo fuertemente desaconsejado.
Un endpoint de Java puede analizar y vincular el identificador de esta forma:
int productId;
try {
productId = Integer.parseInt(request.getParameter("id"));
} catch (NumberFormatException ex) {
response.sendError(400, "Invalid product id");
return;
}
PreparedStatement ps = connection.prepareStatement(
"SELECT name, price FROM products WHERE id = ?"
);
ps.setInt(1, productId);
ResultSet results = ps.executeQuery();
El análisis mejora la comprobación de tipos. El vínculo impide que el valor se convierta en sintaxis SQL. Necesitas ambas cosas.
PHP con PDO puede utilizar un parámetro con nombre:
$stmt = $dbh->prepare(
"SELECT name, price FROM products WHERE id = :id"
);
$stmt->bindValue(':id', $productId, PDO::PARAM_INT);
if (!$stmt->execute()) {
throw new RuntimeException('Database query failed');
}
$results = $stmt->fetchAll();
Un ORM no es un límite de seguridad. Su API parametrizada puede ser segura; su vía de escape para consultas sin procesar sigue siendo SQL sin procesar. Revisa las llamadas denominadas raw, execute, fromSql o similares, junto con la interpolación de strings dentro de los constructores de consultas.
El mismo principio se aplica a los parámetros con nombre de Hibernate, las condiciones de ActiveRecord y las API equivalentes en C# y Rust. Un método llamado bind no demuestra nada por sí solo. Sigue una solicitud a través del driver o de una prueba de integración y determina si el límite de la base de datos recibe parámetros separados o una consulta raw concatenada. Algunas bibliotecas emulan sentencias preparadas u ofrecen modos que cambian cómo se realiza la preparación; el requisito es que los datos del usuario no puedan alterar la sintaxis SQL.
Los placeholders vinculan valores. Por lo general, no pueden vincular un nombre de tabla, un nombre de columna ni una dirección de ordenación. La variación estructural necesita una allow-list.
El SQL dinámico necesita allow-lists, no escapes ingeniosos
Si un informe acepta name o price como campo de ordenación, asigna esas opciones a constantes:
String column;
switch (sortField) {
case "name":
column = "name";
break;
case "price":
column = "price";
break;
default:
throw new IllegalArgumentException("Unsupported sort field");
}
String direction = descending ? "DESC" : "ASC";
String sql =
"SELECT name, price FROM products ORDER BY "
+ column + " " + direction;
Cada fragmento SQL procede del código fuente. La entrada del usuario selecciona una opción; nunca se convierte en sintaxis SQL.
Aplica la misma regla a la selección de tablas y columnas. OWASP advierte que el control por parte del usuario de los nombres de tablas o columnas indica un diseño deficiente y puede justificar una reescritura. Si los usuarios pueden elegir estructuras arbitrarias de la base de datos, inspecciona esa autorización y ese límite del esquema antes de añadir otra función de escaping.
Los procedimientos almacenados pueden ser tan eficaces como las sentencias preparadas cuando utilizan SQL fijo y parámetros tipados. El SQL dinámico dentro de un procedimiento necesita la misma disciplina que el SQL dinámico en el código de la aplicación. Esta es la llamada relevante dentro de un procedimiento existente de SQL Server, donde @UserID y @Dept ya se han proporcionado como variables del procedimiento:
DECLARE @sql NVARCHAR(200);
SELECT @sql =
N'SELECT balance FROM accounts_table
WHERE user_ID = @UID AND department = @DPT';
EXEC sp_executesql
@sql,
N'@UID NVARCHAR(20), @DPT NVARCHAR(10)',
@UID = @UserID, @DPT = @Dept;
Los valores están vinculados. Los identificadores interpolados seguirían necesitando una asignación fija.
Esos procedimientos no son automáticamente más seguros: si la aplicación los ejecuta como db_owner, un compromiso exitoso sigue comenzando con control total de la base de datos. Inspecciona el contexto de ejecución real, los permisos del procedimiento, el encadenamiento de propiedad y el comportamiento de suplantación. En SQL Server, revisa los procedimientos en busca de patrones de ejecución dinámica como sp_execute, execute y exec.
Usa una checklist de lanzamiento que cubra el código y los daños
La parametrización gestiona el fallo central entre código y datos. La revisión de lanzamiento debería demostrar que alcanza todas las rutas de consulta y que un error independiente no puede entregar toda la base de datos al proceso web.
- Vincula cada valor en el servidor. Sigue la entrada de la solicitud a través de repositorios y constructores de consultas. Comprueba también los trabajos en segundo plano. Comprueba también los procedimientos almacenados. Confirma que los datos del usuario no puedan alterar la sintaxis SQL.
- Asigna cada elección estructural. Los nombres de tablas, los nombres de columnas y
ASCoDESCdeben proceder de opciones fijas del lado del servidor. Aplica en el servidor validaciones de tipo, longitud, rango y opciones finitas. - Encuentra las vías de escape de SQL raw. Revisa los métodos del ORM, los strings interpolados, las migraciones, los constructores de informes y las funciones auxiliares de la base de datos. No deduzcas seguridad a partir de un método llamado
bind. - Revisa los privilegios de los procedimientos almacenados. Confirma los parámetros tipados, el SQL dinámico seguro y los permisos de ejecución limitados. La cuenta de la aplicación en producción no debería ser
db_owner. - Limita y observa los fallos. Devuelve errores externos genéricos y protege los logs detallados. Genera alertas sobre errores repetidos de la base de datos y un volumen de consultas inusual. Vigila también patrones de acceso inesperados.
- Reproduce una prueba segura. En staging, utiliza un marcador preparado deliberadamente y registra la solicitud, la respuesta, la telemetría y el punto de detención. Mantén las credenciales, los tokens y los registros de producción fuera de la prueba.
Si no puedes rastrear un valor hasta un vínculo del lado del servidor y reproducir una prueba segura en staging, detén el lanzamiento.