Entendiendo la Inyección SQL
La Inyección SQL ocurre cuando los atacantes insertan código SQL malicioso en consultas a través de entrada del usuario. Puede llevar al robo de datos, manipulación de datos y compromiso completo de la base de datos.
Cómo Funciona la Inyección SQL
// VULNERABLE: Concatenación de strings
const username = req.body.username;
const query = "SELECT * FROM users WHERE username = '" + username + "'";
// Entrada normal: "juan"
// Consulta: SELECT * FROM users WHERE username = 'juan'
// Entrada maliciosa: "' OR '1'='1"
// Consulta: SELECT * FROM users WHERE username = '' OR '1'='1'
// ¡Esto retorna TODOS los usuarios!
// Aún peor: "'; DROP TABLE users; --"
// Consulta: SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
// ¡Esto ELIMINA toda la tabla de usuarios!
Prevención: Consultas Parametrizadas
1. Node.js con pg (PostgreSQL)
const { Pool } = require('pg');
const pool = new Pool();
// VULNERABLE
async function getUser(username) {
const query = `SELECT * FROM users WHERE username = '${username}'`;
return pool.query(query);
}
// SEGURO: Consulta parametrizada
async function getUser(username) {
const query = 'SELECT * FROM users WHERE username = $1';
return pool.query(query, [username]);
}
// SEGURO: Múltiples parámetros
async function authenticateUser(username, password) {
const query = 'SELECT * FROM users WHERE username = $1 AND password_hash = $2';
const passwordHash = await bcrypt.hash(password, 10);
return pool.query(query, [username, passwordHash]);
}
2. MySQL con mysql2
const mysql = require('mysql2/promise');
// SEGURO: Usando placeholders
async function getUser(username) {
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);
return rows[0];
}
// SEGURO: Placeholders nombrados
async function searchUsers(criteria) {
const [rows] = await connection.execute(
'SELECT * FROM users WHERE name LIKE :name AND age > :age',
{ name: `%${criteria.name}%`, age: criteria.minAge }
);
return rows;
}
3. Usando ORMs (Prisma)
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Prisma automáticamente usa consultas parametrizadas
async function getUser(username: string) {
return prisma.user.findUnique({
where: { username },
});
}
async function searchUsers(name: string, minAge: number) {
return prisma.user.findMany({
where: {
name: { contains: name },
age: { gte: minAge },
},
});
}
// Incluso las consultas raw son seguras con Prisma
async function customQuery(userId: string) {
return prisma.$queryRaw`
SELECT * FROM users WHERE id = ${userId}
`;
}
4. Usando ORMs (Drizzle)
import { eq, like, gte, and } from 'drizzle-orm';
import { db } from './db';
import { users } from './schema';
// Consultas seguras con Drizzle
async function getUser(username: string) {
return db.select().from(users).where(eq(users.username, username));
}
async function searchUsers(name: string, minAge: number) {
return db.select().from(users).where(
and(
like(users.name, `%${name}%`),
gte(users.age, minAge)
)
);
}
Validación de Entrada
import { z } from 'zod';
// Definir esquemas estrictos
const userQuerySchema = z.object({
username: z.string()
.min(3)
.max(30)
.regex(/^[a-zA-Z0-9_]+$/), // Solo alfanumérico y guión bajo
limit: z.number().int().min(1).max(100).default(10),
offset: z.number().int().min(0).default(0),
});
async function searchUsers(input: unknown) {
// Validar y sanitizar entrada
const { username, limit, offset } = userQuerySchema.parse(input);
// Ahora seguro para usar en consulta
return prisma.user.findMany({
where: { username: { contains: username } },
take: limit,
skip: offset,
});
}
Procedimientos Almacenados
-- Crear un procedimiento almacenado
CREATE PROCEDURE GetUserByUsername(IN p_username VARCHAR(50))
BEGIN
SELECT * FROM users WHERE username = p_username;
END;
-- Llamar desde Node.js
async function getUser(username) {
const [rows] = await connection.execute('CALL GetUserByUsername(?)', [username]);
return rows[0];
}
Defensa en Profundidad
// 1. Usar consultas parametrizadas (defensa primaria)
const query = 'SELECT * FROM users WHERE id = $1';
// 2. Validar y sanitizar entrada
const userId = parseInt(req.params.id, 10);
if (isNaN(userId) || userId < 1) {
throw new Error('ID de usuario inválido');
}
// 3. Usar cuentas de base de datos con mínimo privilegio
// Crear usuario de solo lectura para consultas SELECT
// CREATE USER 'readonly'@'localhost' IDENTIFIED BY 'password';
// GRANT SELECT ON mydb.* TO 'readonly'@'localhost';
// 4. Escapar salida (defensa contra inyección de segundo orden)
function escapeForSQL(str) {
return str.replace(/'/g, "''");
}
// 5. Usar Web Application Firewall (WAF)
// Configurar WAF para detectar patrones de inyección SQL
Pruebas de Inyección SQL
// Payloads comunes de prueba de inyección SQL
const sqlInjectionPayloads = [
"' OR '1'='1",
"' OR '1'='1' --",
"'; DROP TABLE users; --",
"1; SELECT * FROM users",
"' UNION SELECT * FROM passwords --",
"admin'--",
"1' AND '1'='1",
];
// Usar herramientas como sqlmap para pruebas exhaustivas
// sqlmap -u "http://example.com/user?id=1" --dbs
Lista de Verificación de Prevención de Inyección SQL
- Siempre usa consultas parametrizadas o sentencias preparadas
- Usa un ORM que maneje el escape automáticamente
- Valida toda entrada con esquemas estrictos
- Usa cuentas de base de datos con mínimo privilegio
- Mantén el software de base de datos actualizado
- Monitorea y registra consultas de base de datos
- Usa un Web Application Firewall