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
- Mínimos privilegios: el usuario de MySQL de tu aplicación no necesita permisos para
DROPni para ver otras bases de datos. - Validación de tipos: si esperas un id, conviértelo con
(int)o valida conFILTER_VALIDATE_INT. - Errores genéricos: no muestres los mensajes de error de SQL al usuario: dan pistas sobre la estructura de tus tablas.
- Desactivar la emulación: con
PDO::ATTR_EMULATE_PREPARES => falselas consultas se preparan realmente en el servidor MySQL.
Mitos que conviene olvidar
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.
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