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 payloads mostrados 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

  1. Qué es la inyección y por qué ocurre
  2. Inyección SQL (SQLi) en la API de BazarNube
  3. Consultas parametrizadas y ORM
  4. Inyección de comandos del sistema operativo
  5. Inyección LDAP y NoSQL
  6. Validación de entrada y menor privilegio
  7. XSS: la inyección del navegador (remitido a 03-04)
  8. Errores comunes, ejercicios y soluciones

  1. 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.

  1. 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:

?q=x%' UNION SELECT id, email, pwd FROM users --

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.

  1. 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] });

  1. 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.

  1. 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}) con pass tomado 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.

  1. Validación de entrada y menor privilegio

La parametrización es la defensa principal, pero se refuerza con defensa en profundidad:

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. 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 query concatenada. 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

Módulo 3: OWASP Top Ten 2021 en Profundidad

Módulo 4: OWASP ASVS (Application Security Verification Standard)

Módulo 5: OWASP SAMM (Software Assurance Maturity Model)

Módulo 6: OWASP ZAP (Zed Attack Proxy)

Módulo 7: Buenas Prácticas y Recomendaciones

Módulo 8: Ejercicios Prácticos y Casos de Estudio

Módulo 9: Evaluación y Certificación

© Copyright 2026. Todos los derechos reservados