Llegamos a una de las familias de vulnerabilidades más antiguas y persistentes. A03 – Inyección (Injection) ocurre cuando datos controlados por el usuario se mezclan con una instrucción que un intérprete va a ejecutar —SQL, comandos del sistema operativo, LDAP, consultas NoSQL— y el intérprete acaba ejecutando parte de esos datos como si fueran instrucciones. En el Top Ten 2021 esta categoría creció: absorbió al antiguo Cross-Site Scripting (XSS), que técnicamente es una inyección en el navegador. Aquí trataremos la inyección del lado servidor; el XSS lo desarrollamos a fondo en la lección 03-04, así que en esta solo lo mencionamos como pariente cercano.
La raíz siempre es la misma: no separar el código de los datos. En BazarNube veremos una inyección SQL en el buscador de productos de la API Node/PostgreSQL, y una inyección de comandos en una utilidad del módulo legacy Java. El impacto va desde leer toda la base de datos hasta ejecutar comandos en el servidor.
Aviso legal y ético: los
payloadsmostrados son ilustrativos y con datos ficticios. Practica solo sobre sistemas propios o con autorización explícita. Lanzar inyecciones contra sistemas ajenos es delito.
Contenido
- Qué es la inyección y por qué ocurre
- Inyección SQL (SQLi) en la API de BazarNube
- Consultas parametrizadas y ORM
- Inyección de comandos del sistema operativo
- Inyección LDAP y NoSQL
- Validación de entrada y menor privilegio
- XSS: la inyección del navegador (remitido a 03-04)
- Errores comunes, ejercicios y soluciones
- Qué es la inyección y por qué ocurre
Un intérprete recibe una cadena y decide qué es instrucción y qué es dato. Si construimos esa cadena concatenando entrada del usuario, este puede introducir metacaracteres (', ;, --, $) que rompen la estructura prevista y cambian el significado de la instrucción.
flowchart LR
A[Entrada del usuario] --> B{Se concatena en la instruccion?}
B -- Si --> C[El interprete ejecuta datos como codigo]
B -- No, va como parametro --> D[El dato nunca cambia la estructura]
C --> E[Inyeccion]
D --> F[Seguro]
La defensa de fondo es separar código de datos: el desarrollador define la estructura de la instrucción y el motor trata la entrada del usuario siempre como valor, nunca como sintaxis.
- Inyección SQL en BazarNube
El buscador de productos de la API se escribió concatenando el término de búsqueda:
// routes/search.js — VULNERABLE a SQLi
router.get('/api/products/search', async (req, res) => {
const q = req.query.q;
const sql = "SELECT id, name, price FROM products WHERE name LIKE '%" + q + "%'";
const result = await db.query(sql); // concatenacion directa
res.json(result.rows);
});Cómo se explota (ilustrativo)
Con una búsqueda normal ?q=camiseta, la consulta es correcta. Pero un atacante envía:
La consulta resultante se convierte en:
SELECT id, name, price FROM products WHERE name LIKE '%x%'
UNION SELECT id, email, pwd FROM users --%'El UNION añade a los resultados los correos y hashes de contraseñas de la tabla users, y -- comenta el resto. El buscador de productos acaba filtrando credenciales. Variantes de esta técnica permiten leer cualquier tabla, borrar datos (; DROP TABLE ...) o extraer información a ciegas (blind SQLi) midiendo tiempos de respuesta.
Corrección: consultas parametrizadas
// routes/search.js — SEGURO con consulta parametrizada
router.get('/api/products/search', async (req, res) => {
const q = req.query.q ?? '';
const sql = 'SELECT id, name, price FROM products WHERE name LIKE $1';
const result = await db.query(sql, ['%' + q + '%']); // q va como PARAMETRO
res.json(result.rows);
});La diferencia es esencial: $1 es un marcador de posición. El driver de PostgreSQL envía la estructura de la consulta y los valores por separado; el motor nunca interpreta el contenido de q como SQL. Aunque q contenga ' UNION SELECT ..., se buscará literalmente ese texto en el nombre del producto. El código y los datos quedan separados.
- Consultas parametrizadas y ORM
| Enfoque | Ejemplo | Seguridad |
|---|---|---|
| Concatenación de cadenas | "... WHERE id=" + id |
Vulnerable |
| Consulta parametrizada | query(sql, [id]) |
Seguro |
| ORM / query builder | Product.findByPk(id) |
Seguro (usa parámetros por debajo) |
Un ORM como Sequelize o Prisma genera consultas parametrizadas automáticamente, lo que reduce el riesgo. Ojo: esa protección se pierde si usas la "vía de escape" del ORM para SQL crudo concatenando entrada:
// PELIGRO: incluso con ORM, esto vuelve a ser vulnerable
sequelize.query("SELECT * FROM products WHERE name = '" + name + "'");
// Correcto: usar replacements/bind
sequelize.query('SELECT * FROM products WHERE name = ?', { replacements: [name] });
- Inyección de comandos del sistema operativo
El módulo legacy Java tiene una utilidad que genera miniaturas invocando una herramienta externa. Se construyó concatenando el nombre de fichero:
// ThumbnailService.java — VULNERABLE a inyeccion de comandos
public void generate(String filename) throws IOException {
// filename llega desde una peticion del usuario
String cmd = "convert /uploads/" + filename + " -resize 100x100 /thumbs/" + filename;
Runtime.getRuntime().exec(cmd); // ejecuta una cadena en la shell
}Si filename es foto.png; rm -rf /data, la shell ejecuta dos comandos. El atacante logra ejecución de comandos en el servidor. Corrección: no invocar una shell y pasar los argumentos por separado, además de validar el nombre.
// ThumbnailService.java — SEGURO
public void generate(String filename) throws IOException, InterruptedException {
if (!filename.matches("[A-Za-z0-9_.-]{1,64}")) { // allowlist estricta
throw new IllegalArgumentException("Nombre de fichero invalido");
}
ProcessBuilder pb = new ProcessBuilder(
"convert", "/uploads/" + filename, "-resize", "100x100", "/thumbs/" + filename);
// ProcessBuilder NO usa shell: cada argumento se pasa tal cual, sin interpretar ; | &
pb.start().waitFor();
}ProcessBuilder con argumentos separados evita que la shell interprete metacaracteres, y la lista blanca (matches) rechaza cualquier nombre que no sea alfanumérico. Doble barrera.
- Inyección LDAP y NoSQL
El mismo principio (separar código de datos) aplica a otros intérpretes:
- LDAP: construir filtros concatenando entrada permite inyectar
*)(uid=*para saltar autenticaciones. Solución: escapar según RFC 4515 o usar APIs que parametricen el filtro. - NoSQL (MongoDB): enviar objetos en lugar de cadenas permite operadores. Si el login hace
find({user, pass})conpasstomado tal cual del JSON, un atacante manda{"pass": {"$ne": null}}y consigue "cualquier contraseña distinta de null".
// NoSQL — VULNERABLE: acepta objetos del cliente
db.users.findOne({ user: req.body.user, pass: req.body.pass });
// SEGURO: forzar tipos primitivos antes de consultar
const user = String(req.body.user), pass = String(req.body.pass);
db.users.findOne({ user, pass }); // (y ademas comparar hash, ver A02)Forzar los tipos (String(...)) impide que el cliente cuele operadores como $ne o $gt.
- Validación de entrada y menor privilegio
La parametrización es la defensa principal, pero se refuerza con defensa en profundidad:
- Validación de entrada por lista blanca: acepta solo lo esperado (tipo, longitud, formato). No confíes en "listas negras" de caracteres prohibidos; siempre hay una forma de saltarlas.
- Menor privilegio en la base de datos: la cuenta que usa la API de BazarNube no necesita ser superusuario ni poder
DROP TABLE. Si solo lee/escribe en ciertas tablas, una SQLi tiene mucho menos alcance. - Escapado como último recurso: cuando no puedas parametrizar (p. ej. un nombre de columna dinámico), valida contra una lista blanca de valores permitidos, nunca concatenes directamente.
- Limitar resultados y errores: no devuelvas errores SQL crudos al cliente (facilitan la explotación; ver A05).
| Capa | Qué aporta |
|---|---|
| Consulta parametrizada / ORM | Elimina la inyección en origen |
| Validación por allowlist | Rechaza entrada malformada antes de llegar al motor |
| Menor privilegio en BD | Reduce el impacto si algo falla |
| Errores genéricos | No dan pistas al atacante |
- XSS: la inyección del navegador
El Cross-Site Scripting también es inyección, pero el intérprete es el navegador (ejecuta JavaScript inyectado) en lugar de la base de datos. Por su importancia y por sus técnicas propias (codificación de salida contextual, CSP, escapes de React) lo tratamos en su propia lección: 03-04 – Cross-Site Scripting (XSS) en Profundidad. Aquí solo dejamos constancia de que comparte la misma raíz: mezclar datos del usuario con código que otro motor va a ejecutar.
Errores Comunes y Consejos
- Concatenar entrada en consultas. Aunque "parezca un número", parametriza siempre.
- Confiar en listas negras de caracteres. Son evitables; usa listas blancas y parametrización.
- Escapar a mano el SQL. Es frágil; deja el escapado al driver mediante parámetros.
- Usar el ORM y luego caer en
raw queryconcatenada. Anula toda la protección. - Pasar entrada a una shell. Evita
Runtime.exec(string)/child_process.exec; usa APIs con argumentos separados. - Cuenta de BD con permisos totales. Aplica menor privilegio: limita tablas y operaciones.
- Consejo: habilita un escáner SAST/DAST en CI (lo veremos con ZAP en M6) para detectar concatenaciones peligrosas.
Ejercicios
Ejercicio 1. Reescribe de forma segura este endpoint de BazarNube:
router.get('/api/orders', async (req, res) => {
const status = req.query.status;
const rows = await db.query(
"SELECT * FROM orders WHERE user_id = " + req.session.userId +
" AND status = '" + status + "'");
res.json(rows.rows);
});Ejercicio 2. El login del módulo Java usa "... WHERE user='" + u + "' AND pass='" + p + "'". Muestra un payload que salte la autenticación y explica por qué la parametrización lo evita.
Ejercicio 3. ¿Por qué ProcessBuilder("convert", filename, ...) es más seguro que Runtime.exec("convert " + filename)? ¿Sigue siendo recomendable validar filename?
Soluciones
Solución 1. Parametrizar ambos valores (incluido user_id, aunque venga de la sesión, por consistencia):
router.get('/api/orders', async (req, res) => {
const rows = await db.query(
'SELECT * FROM orders WHERE user_id = $1 AND status = $2',
[req.session.userId, req.query.status]);
res.json(rows.rows);
});Solución 2. Con u = admin' -- la consulta pasa a ... WHERE user='admin' --' AND pass='...': el -- comenta la comprobación de contraseña y el atacante entra como admin. Con parámetros, admin' -- se buscaría literalmente como nombre de usuario (no existe), porque el motor nunca interpreta esa cadena como sintaxis SQL.
Solución 3. ProcessBuilder con argumentos separados no lanza una shell, así que metacaracteres como ;, | o & se pasan como texto literal a convert, no se interpretan como separadores de comandos. Aun así conviene validar filename por lista blanca: evita rutas maliciosas (../../etc/passwd) y entradas que rompan la herramienta. Defensa en profundidad.
Conclusión
La inyección se derrota separando código de datos: consultas parametrizadas o ORM para SQL, APIs con argumentos separados para comandos del SO, forzado de tipos para NoSQL y escapado adecuado para LDAP, todo reforzado con validación por lista blanca y menor privilegio. Es una defensa mecánica y muy efectiva: bien aplicada, elimina la categoría casi por completo.
Entrada de backlog — A03: parametrizado el buscador de productos y el listado de pedidos; sustituido Runtime.exec por ProcessBuilder con validación en ThumbnailService; forzado de tipos en el login NoSQL; creada una cuenta de BD con permisos mínimos para la API.
Hemos mencionado que el XSS es la inyección que se ejecuta en el navegador. Es tan relevante para el front React de BazarNube que le dedicamos la siguiente lección completa: Cross-Site Scripting (XSS) en Profundidad.
Curso de OWASP: Directrices y Estándares para la Seguridad en Aplicaciones Web
Módulo 1: Introducción a OWASP
Módulo 2: Principales Proyectos de OWASP
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Otros Proyectos Clave: WSTG, Cheat Sheets y Dependency-Check
Módulo 3: OWASP Top Ten 2021 en Profundidad
- A01:2021 – Pérdida de Control de Acceso
- A02:2021 – Fallos Criptográficos y Exposición de Datos Sensibles
- A03:2021 – Inyección
- Cross-Site Scripting (XSS) en Profundidad
- A04:2021 – Diseño Inseguro
- A05:2021 – Configuración de Seguridad Incorrecta
- Entidades Externas XML (XXE)
- A06:2021 – Componentes Vulnerables y Desactualizados
- A07:2021 – Fallos de Identificación y Autenticación
- A08:2021 – Fallos de Integridad de Software y Datos (Deserialización Insegura)
- A09:2021 – Fallos de Registro y Monitorización
- A10:2021 – Server-Side Request Forgery (SSRF)
Módulo 4: OWASP ASVS (Application Security Verification Standard)
- Introducción a ASVS
- Niveles de Verificación
- Requisitos de Seguridad
- Implementación de ASVS en Proyectos
Módulo 5: OWASP SAMM (Software Assurance Maturity Model)
Módulo 6: OWASP ZAP (Zed Attack Proxy)
- Introducción a ZAP
- Instalación y Configuración
- Escaneo de Vulnerabilidades
- Automatización de Pruebas de Seguridad
Módulo 7: Buenas Prácticas y Recomendaciones
- Ciclo de Vida de Desarrollo Seguro (SDLC)
- Modelado de Amenazas (Threat Modeling)
- Integración de Seguridad en DevOps (DevSecOps)
- Capacitación y Concienciación en Seguridad
- Herramientas y Recursos Adicionales
Módulo 8: Ejercicios Prácticos y Casos de Estudio
- Ejercicio 1: Identificación de Vulnerabilidades
- Ejercicio 2: Implementación de Controles de Seguridad
- Caso de Estudio 1: Análisis de un Incidente de Seguridad
- Caso de Estudio 2: Mejora de la Seguridad en una Aplicación Web
