TechLead
Lección 4 de 9
5 min de lectura
Seguridad Web

Prevención de Inyección SQL

Entiende los ataques de inyección SQL y aprende a prevenirlos con consultas parametrizadas y ORMs.

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

Continuar Aprendiendo