libro / blog / prevenir-inyeccion-sql

// capítulo 08 · seguridad

Cómo prevenir la inyección SQL en PHP con consultas preparadas

Gustavo Arias8 min de lectura

TL;DR Nunca concatenes datos del usuario dentro de una consulta SQL. Usa siempre consultas preparadas (prepare + execute). Para lo que no se puede parametrizar, como nombres de columnas en un ORDER BY, usa una lista blanca.

La inyección SQL lleva décadas entre las vulnerabilidades web más explotadas, y sigue presente en la lista OWASP Top 10 dentro de la categoría de inyección. La buena noticia es que evitarla es sencillo si adoptas un hábito desde el primer día. Por eso en el libro Programación Full-Stack el capítulo de consultas seguras va justo después de la conexión a MySQL, antes de escribir un solo formulario de login.

Qué es la inyección SQL

Ocurre cuando datos que controla el usuario se mezclan con el código SQL y el motor de base de datos los interpreta como instrucciones. El atacante no “rompe” nada: simplemente escribe SQL donde tú esperabas un email o un número.

Anatomía de un ataque

Este login es vulnerable. Desgraciadamente, todavía aparece en tutoriales:

<?php
$email = $_POST['email'];
$pass  = $_POST['password'];

// ✗ NUNCA hagas esto
$sql = "SELECT * FROM usuarios WHERE email = '$email' AND password = '$pass'";
$usuario = $pdo->query($sql)->fetch();

Si alguien escribe en el campo email el texto ' OR '1'='1' -- , la consulta que llega a MySQL es:

SELECT * FROM usuarios WHERE email = '' OR '1'='1' -- ' AND password = '...'

'1'='1' siempre es verdadero y -- comenta el resto, incluida la comprobación de la contraseña. Resultado: el atacante entra como el primer usuario de la tabla, que suele ser el administrador. Con variantes del mismo truco se pueden leer otras tablas o borrar datos.

La solución: consultas preparadas

Una consulta preparada separa en dos pasos el código SQL y los datos. Primero envías la plantilla con marcadores; después, los valores. MySQL ya sabe qué es instrucción y qué es dato, así que por mucho SQL que escriba el usuario, se trata como texto.

<?php
$stmt = $pdo->prepare('SELECT id, nombre, password_hash FROM usuarios WHERE email = :email');
$stmt->execute(['email' => $_POST['email'] ?? '']);
$usuario = $stmt->fetch();

// La contraseña se verifica en PHP contra el hash, no en la consulta
if ($usuario && password_verify($_POST['password'] ?? '', $usuario['password_hash'])) {
    // login correcto
}

Además del cambio de query a prepare, fíjate en otra mejora: las contraseñas no se comparan en SQL. Se guardan como hash y se verifican con password_verify, como explicamos en el artículo sobre login seguro.

Marcadores con nombre o posicionales

// Con nombre: más legible cuando hay muchos parámetros
$stmt = $pdo->prepare('INSERT INTO productos (nombre, precio) VALUES (:nombre, :precio)');
$stmt->execute(['nombre' => $nombre, 'precio' => $precio]);

// Posicionales: el orden importa
$stmt = $pdo->prepare('UPDATE productos SET stock = ? WHERE id = ?');
$stmt->execute([$stock, $id]);

Casos especiales: IN, LIKE y ORDER BY

Búsquedas con LIKE

// Los comodines % van en el valor, no en la consulta
$stmt = $pdo->prepare('SELECT * FROM productos WHERE nombre LIKE :q');
$stmt->execute(['q' => '%' . $busqueda . '%']);

Listas con IN

$ids = [3, 7, 12];
$marcadores = implode(',', array_fill(0, count($ids), '?')); // "?,?,?"
$stmt = $pdo->prepare("SELECT * FROM productos WHERE id IN ($marcadores)");
$stmt->execute($ids);

ORDER BY y nombres de columnas: lista blanca

Los marcadores solo sirven para valores, no para nombres de tablas, columnas o palabras como ASC. Si el usuario elige por qué columna ordenar, compara su elección contra una lista de opciones permitidas:

$columnas = ['nombre' => 'nombre', 'precio' => 'precio', 'fecha' => 'creado_en'];
$orden = $columnas[$_GET['orden'] ?? ''] ?? 'nombre';    // valor por defecto seguro
$dir   = ($_GET['dir'] ?? '') === 'desc' ? 'DESC' : 'ASC';

$productos = $pdo->query("SELECT * FROM productos ORDER BY $orden $dir")->fetchAll();

Aquí concatenar es seguro porque $orden y $dir solo pueden tomar valores que escribiste tú.

Capas extra de defensa

Mitos que conviene olvidar

“Uso addslashes o mysqli_real_escape_string”. Escapar a mano es frágil: basta un campo olvidado o un número sin comillas para que falle. Las consultas preparadas no dependen de que te acuerdes de nada.
“Los datos vienen de un select, así que son seguros”. Cualquier valor de un formulario se puede modificar desde las DevTools o enviarse con herramientas como curl. Todo lo que llega en la petición es dato del usuario.
Más información en la guía de prevención de inyección SQL de OWASP (en inglés).

Seguridad desde el primer proyecto

En el libro Programación Full-Stack las consultas seguras llegan antes del login y del CRUD, para que construyas tus proyectos con buenos hábitos desde el principio.

Comprar en Amazon