Introducción: la pregunta que atormenta a todo desarrollador PHP
\n\nSi alguna vez has escrito consultas SQL en PHP, seguramente has escuchado el mantra: "Usa PDO y prepared statements — y estarás seguro". Pero, ¿es realmente así? En Stack Overflow, la pregunta "Are PDO prepared statements sufficient to prevent SQL injection?" ha acumulado miles de visitas y decenas de acalorados debates. Y no es para menos.
\n\nEn este artículo no solo daremos una respuesta corta de "sí" o "no". Nos meteremos bajo el capó de PDO, analizaremos cómo funcionan exactamente las consultas preparadas y descubriremos en qué casos incluso PDO puede fallarte. ¿Listos? Allá vamos.
\n\n¿Qué es una inyección SQL y por qué es peligrosa?
\n\nAntes de hablar de protección, recordemos al enemigo en persona. La inyección SQL es un ataque en el que un atacante "inyecta" código SQL malicioso a través de datos de entrada. Un clásico:
\n\n// Código malo: nunca lo hagas así\n$username = $_POST['username'];\n$query = "SELECT * FROM users WHERE username = '$username'";\n$result = mysqli_query($conn, $query);\n\n\nSi el atacante introduce admin' OR '1'='1, la consulta se convierte en:
SELECT * FROM users WHERE username = 'admin' OR '1'='1'\n\n\n¡Voilà! Acceso a datos sin contraseña. ¿Y si introduce '; DROP TABLE users; --? Adiós, tabla de usuarios.
PDO y consultas preparadas: ¿cómo funciona?
\n\nPDO (PHP Data Objects) ofrece un mecanismo de prepared statements (expresiones preparadas). La idea es simple: separas la estructura de la consulta de los datos. En lugar de insertar variables directamente en la cadena, usas marcadores de posición:
\n\n// Código seguro con PDO\n$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");\n$stmt->execute([':username' => $_POST['username']]);\n\n\nPero, ¿qué sucede bajo el capó? Este es el punto clave que muchos pasan por alto.
\n\nModos de emulación vs. prepared statements nativos
\n\nPDO puede funcionar en dos modos:
\n\n- \n
- Modo de emulación (por defecto en versiones antiguas de PHP) — PDO escapa los datos por sí mismo y los inserta en la consulta del lado de PHP. Sigue siendo seguro, pero teóricamente vulnerable si el escape falla (por ejemplo, al trabajar con codificaciones no estándar). \n
- Modo nativo — PDO envía al servidor de BD dos mensajes: primero la plantilla de la consulta, luego los datos por separado. El servidor procesa los datos como valores, no como código. Esto es absolutamente seguro. \n
Para activar el modo nativo, desactiva la emulación:
\n\n$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);\n\n\nAhora la consulta se envía al servidor en dos etapas. Incluso si el atacante introduce ' OR '1'='1, el servidor MySQL o PostgreSQL simplemente lo interpretará como un valor de cadena, no como parte del código SQL. ¡Magia!
Pero, ¿es todo tan perfecto? Problemas ocultos de PDO
\n\nVolvamos a la pregunta de Stack Overflow. La respuesta es "sí, los prepared statements de PDO son suficientes" — pero con salvedades. Estas son situaciones en las que la protección puede fallar.
\n\n1. Identificadores dinámicos (nombres de tablas, columnas)
\n\nLos prepared statements solo funcionan para valores (datos). Si insertas el nombre de una tabla o columna, los marcadores de posición no ayudan:
\n\n// ¡Peligroso! El nombre de la tabla no se puede pasar mediante un marcador de posición\n$stmt = $pdo->prepare("SELECT * FROM :table WHERE id = :id");\n$stmt->execute([':table' => $_GET['table'], ':id' => 1]); // ¡Error!\n\n\nPDO simplemente insertará el nombre de la tabla como una cadena y la consulta fallará. Si usas emulación, el atacante podría introducir users; DROP TABLE users; --. Solución: lista blanca de valores permitidos.
2. Consultas LIKE y caracteres comodín
\n\nSi usas