La inyección SQL es una de las vulnerabilidades más antiguas de la web — y sigue siendo una de las más devastadoras. A pesar de ser conocida desde hace más de 25 años, sigue apareciendo en el OWASP Top 10. Un solo campo de búsqueda mal protegido puede dar acceso completo a tu base de datos.
¿Qué es la inyección SQL?
Un ataque de inyección SQL ocurre cuando un atacante introduce código SQL malicioso a través de un input de usuario que la aplicación inserta directamente en una consulta SQL sin sanitizar.
Ejemplo clásico
// ❌ VULNERABLE — concatenación directa
const query = `SELECT * FROM users WHERE email = '${email}' AND passwd = '${passwd}'`;
Un atacante introduce como email: [email protected]' --
La consulta resultante:
SELECT * FROM users WHERE email = '[email protected]' --' AND passwd = 'lo_que_sea'
El -- comenta el resto de la consulta. El atacante accede como admin sin conocer la contraseña.
Tipos de inyección SQL
In-band (clásica)
El atacante obtiene los resultados directamente en la respuesta de la aplicación.
Union-based: el atacante usa UNION SELECT para extraer datos de otras tablas:
' UNION SELECT username, passwd FROM admin_users --
Error-based: los mensajes de error del servidor revelan la estructura de la BD.
Blind (ciega)
La aplicación no muestra datos pero responde de forma diferente según la consulta sea verdadera o falsa.
Boolean-based: el atacante hace preguntas sí/no:
' AND (SELECT SUBSTRING(passwd,1,1) FROM users WHERE id=1) = 'a' --
Time-based: la aplicación tarda más en responder si la condición es verdadera:
' AND IF(1=1, SLEEP(5), 0) --
Out-of-band
El atacante extrae datos a través de canales alternativos (DNS, HTTP) cuando la respuesta directa no es posible.
Impacto de una inyección SQL
- Exfiltración de datos: acceso a toda la base de datos (usuarios, contraseñas, datos personales, tarjetas)
- Modificación de datos: alteración de precios, permisos, contenido
- Eliminación de datos:
DROP TABLEpuede destruir toda tu información - Escalada de privilegios: el atacante se convierte en admin
- Ejecución de comandos del sistema: en configuraciones inseguras,
xp_cmdshell(SQL Server) puede ejecutar comandos OS - Pivot a otros sistemas: la BD como puerta de entrada a la red interna
Prevención: Queries parametrizadas
La defensa principal contra inyección SQL. Sin excepciones.
Node.js con pg (PostgreSQL)
// ❌ VULNERABLE
const result = await client.query(`SELECT * FROM users WHERE email = '${email}'`);
// ✅ SEGURO — query parametrizada
const result = await client.query('SELECT * FROM users WHERE email = $1', [email]);
Node.js con mysql2
// ✅ SEGURO — query parametrizada
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ? AND status = ?',
[email, 'active']
);
PHP con PDO
// ✅ SEGURO — prepared statement
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
Python con psycopg2
# ✅ SEGURO — query parametrizada
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
Prevención: ORM
Los ORMs (Object-Relational Mapping) generan queries parametrizadas automáticamente:
Prisma
// ✅ SEGURO — Prisma parametriza automáticamente
const user = await prisma.user.findUnique({
where: { email: userInput },
});
Sequelize
// ✅ SEGURO
const users = await User.findAll({
where: { email: userInput },
});
Django ORM
# ✅ SEGURO
user = User.objects.filter(email=user_input).first()
Cuidado: los ORMs protegen contra inyección SQL en sus métodos estándar. Si usas raw queries, la protección desaparece:
// ❌ PELIGROSO incluso con Prisma
const result = await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
// ✅ SEGURO — raw query parametrizada
const result = await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
Defensas adicionales
Validación de inputs
Valida todos los inputs antes de usarlos en consultas:
import { z } from 'zod';
const searchSchema = z.object({
email: z.string().email(),
limit: z.number().int().min(1).max(100).default(10),
page: z.number().int().min(1).default(1),
});
Principio de mínimo privilegio
El usuario de base de datos de tu aplicación NO debe tener permisos de:
DROP TABLECREATE TABLEALTER TABLEGRANT/REVOKE- Acceso a tablas del sistema
-- Crear usuario con permisos mínimos
CREATE USER app_user WITH PASSWD 'strong_passwd';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
-- NO conceder DDL ni admin
WAF (Web Application Firewall)
Un WAF como Cloudflare, AWS WAF o ModSecurity puede detectar y bloquear patrones de inyección SQL en las peticiones antes de que lleguen a tu aplicación.
No sustituye las queries parametrizadas, pero añade una capa de defensa.
Mensajes de error genéricos
Nunca expongas errores SQL al usuario:
// ❌ PELIGROSO
catch (error) {
res.status(500).json({ error: error.message });
// Revela: "relation "users" does not exist"
}
// ✅ SEGURO
catch (error) {
logger.error({ error: error.message }, 'Database query failed');
res.status(500).json({ error: 'Internal server error' });
}
Checklist anti-SQLi
- [ ] Todas las consultas usan queries parametrizadas o ORM
- [ ] Zero raw queries con concatenación de strings
- [ ] Validación de inputs con esquemas (Zod, Joi)
- [ ] Usuario de BD con mínimos privilegios
- [ ] Mensajes de error genéricos en producción
- [ ] WAF configurado con reglas anti-SQLi
- [ ] Escaneos periódicos de vulnerabilidades
- [ ] Backups regulares de la base de datos
Cómo WarDek detecta inyección SQL
WarDek analiza tu aplicación web en busca de vectores de inyección SQL:
- Detección de formularios: identifica inputs que podrían ser vectores de inyección
- Verificación de headers: headers que revelan tecnología de BD
- Análisis de errores: detecta si la aplicación expone errores SQL
- Puntuación OWASP: score específico incluyendo riesgo de inyección
Escanea tu sitio web gratis con WarDek — OWASP, NIS2, RGPD, AI Act en un solo escaneo.