Desglosa XSS, CSRF y la inyección SQL para entender exactamente qué bloquean en realidad htmlspecialchars, los tokens y las consultas preparadas.
Abrir esta lección en KodokonLas tres grandes familias de vulnerabilidades web - XSS, CSRF e inyecciones - comparten la misma causa raíz: los datos proporcionados por el usuario acaban interpretándose como código (HTML, SQL o una petición legítima). Empecemos por XSS: si muestras un valor de $_GET sin escaparlo, un atacante puede inyectar una etiqueta <script> y ejecutar JavaScript en los navegadores de tus demás visitantes - robando sesiones, realizando acciones sin su conocimiento. Con ?comment=<script>alert(1)</script>, la primera salida de abajo ejecuta el script; la segunda lo neutraliza. Aquí importan dos flags: ENT_QUOTES convierte también las comillas simples (de lo contrario, un atributo delimitado por comillas simples sigue siendo inyectable), y ENT_SUBSTITUTE reemplaza las secuencias UTF-8 inválidas en lugar de devolver una cadena vacía. Desde PHP 8.1, ambos flags son por fin el valor por defecto, pero explicitarlos documenta tu intención y protege el código que se ejecuta en versiones anteriores.
<?php
declare(strict_types=1);
$comment = $_GET['comment'] ?? '';
echo '<div>' . $comment . '</div>';
function e(string $value): string
{
return htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
}
echo '<div>' . e($comment) . '</div>';CSRF explota un hecho sencillo: el navegador adjunta automáticamente las cookies de sesión a cualquier petición hacia tu dominio, incluso una desencadenada desde un sitio de terceros. Un formulario hostil puede, por tanto, enviar /account/delete usando la sesión de la víctima. La defensa clásica es el token sincronizador: un secreto aleatorio almacenado en la sesión y exigido en cada petición que modifica datos. Un sitio de terceros no puede ni leerlo ni adivinarlo, por lo que no puede proporcionarlo.
<?php
session_start();
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$sent = $_POST['csrf'] ?? '';
$valid = is_string($sent)
&& hash_equals($_SESSION['csrf'], $sent);
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
if ($isPost && !$valid) {
http_response_code(403);
exit('Invalid CSRF token');
}
?>
<form method="post">
<input type="hidden" name="csrf"
value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
<button>Delete my account</button>
</form>La inyección SQL sigue el mismo patrón: concatenar un valor del usuario en una consulta equivale a dejar que el usuario escriba SQL - ' OR 1=1 -- convierte un filtro en la tabla entera. La solución no es escapar, sino la consulta preparada: el texto de la consulta (con sus marcadores :email) y los valores viajan por separado hasta el motor, que por tanto nunca puede confundir los datos con el código. Un detalle de experto: con MySQL, PDO emula las consultas preparadas por defecto, escapando en el lado del cliente; establece PDO::ATTR_EMULATE_PREPARES en false para tener consultas preparadas realmente nativas.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION
);
$pdo->exec(
'CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL
)'
);
$email = $_GET['email'] ?? '';
$stmt = $pdo->prepare(
'SELECT id FROM users WHERE email = :email'
);
$stmt->execute(['email' => $email]);
var_dump($stmt->fetch(PDO::FETCH_ASSOC));