Introducción: ¿por qué las inyecciones SQL siguen siendo relevantes?
\n\nImagina: tu sitio web funciona como un reloj, los usuarios se registran, dejan comentarios, realizan pedidos. Y de repente, la base de datos termina en manos de un atacante. No porque los hackers sean genios, sino porque alguien olvidó poner un signo de interrogación en una consulta SQL.
\n\nLas inyecciones SQL no son una amenaza arcaica de los años 2000. En 2024, OWASP aún las sitúa entre las 3 vulnerabilidades más peligrosas en aplicaciones web. Y la pregunta "How can I prevent SQL injection in PHP?" en Stack Overflow supera los 1.5 millones de visitas, y sigue creciendo. Esto significa que el problema sigue vigente.
En este artículo, veremos cómo cerrar la puerta de forma definitiva a las inyecciones SQL en PHP en 2025. Sin magia, sin frameworks complejos, solo código limpio y las mejores prácticas.
\n\n¿Qué es una inyección SQL en términos sencillos?
\n\nUna inyección SQL ocurre cuando la entrada del usuario se convierte en parte de un comando SQL, en lugar de ser solo un dato. Ejemplo de la vida real:
\n\n<?php\n// CÓDIGO MALO — nunca hagas esto\n$username = $_POST['username'];\n$query = "SELECT * FROM users WHERE username = '$username'";\n$result = mysqli_query($conn, $query);\n?>\n\nSi un atacante ingresa en el campo username la cadena ' OR '1'='1, la consulta se convierte en:
SELECT * FROM users WHERE username = '' OR '1'='1'\n\nEsto devuelve todos los usuarios. Y si ingresa '; DROP TABLE users; --, podría perder toda la tabla. ¿Da miedo? Claro que sí.
Mejores prácticas para protegerte de inyecciones SQL en PHP 2025
\n\n1. Usa PDO con consultas preparadas (es el estándar)
\n\nPDO (PHP Data Objects) es una interfaz universal para trabajar con bases de datos. Su principal ventaja: separa la lógica SQL de los datos. Escribes la consulta con marcadores de posición (placeholders) y PDO se encarga del escape seguro.
\n\n<?php\n// CÓDIGO BUENO — usa PDO\n$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';\n$pdo = new PDO($dsn, $user, $pass, [\n PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,\n PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n PDO::ATTR_EMULATE_PREPARES => false, // desactivamos la emulación\n]);\n\n$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');\n$stmt->execute(['email' => $_POST['email']]);\n$user = $stmt->fetch();\n?>\n\n¿Por qué funciona? La base de datos recibe la estructura de la consulta por separado de los datos. Incluso si el usuario ingresa SQL malicioso, se interpretará como una cadena de texto normal, no como parte del comando.
\n\n2. Mysqli con consultas preparadas (alternativa)
\n\nSi solo trabajas con MySQL, puedes usar mysqli, pero siempre con consultas preparadas, no con escape mediante mysqli_real_escape_string().
<?php\n$mysqli = new mysqli('localhost', 'user', 'pass', 'testdb');\n$stmt = $mysqli->prepare('INSERT INTO comments (user_id, text) VALUES (?, ?)');\n$stmt->bind_param('is', $userId, $commentText);\n$stmt->execute();\n?>\n\nImportante: en 2025, mysqli_real_escape_string() sigue existiendo, pero confiar en él para protegerte es una mala idea. Solo escapa cadenas, pero no protege contra inyecciones en campos numéricos o en operadores LIKE.
3. Filtrado y validación: segunda línea de defensa
\n\nLas consultas preparadas son la base. Pero añade filtrado por tipo de dato. Si esperas un número, verifica que sea un número. Si esperas un email, verifica el formato.
\n\n<?php\n$id = filter_input(INPUT_POST, 'user_id', FILTER_VALIDATE_INT);\nif ($id === false) {\n throw new InvalidArgumentException('El ID debe ser un número entero');\n}\n\n$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');\n$stmt->execute(['id' => $id]);\n?>\n\nCombinando