Démontez XSS, CSRF et injection SQL pour comprendre exactement ce que bloquent htmlspecialchars, les jetons et les requêtes préparées.
Ouvrir cette leçon dans KodokonLes trois grandes familles de failles web - XSS, CSRF et injections - partagent la même cause : une donnée fournie par l'utilisateur finit interprétée comme du code (HTML, SQL ou requête légitime). Commençons par le XSS : si vous affichez une valeur de $_GET sans échappement, un attaquant peut injecter <script> et exécuter du JavaScript chez vos autres visiteurs - vol de session, actions à leur insu. Avec ?comment=<script>alert(1)</script>, la première sortie ci-dessous exécute le script ; la seconde le neutralise. Deux drapeaux comptent : ENT_QUOTES convertit aussi les apostrophes (sinon un attribut délimité par des guillemets simples reste injectable) et ENT_SUBSTITUTE remplace les séquences UTF-8 invalides au lieu de renvoyer une chaîne vide. Depuis PHP 8.1, ces deux drapeaux sont enfin le défaut, mais les expliciter documente l'intention et protège le code exécuté sur des versions plus anciennes.
<?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>';Le CSRF exploite un fait simple : le navigateur joint automatiquement les cookies de session à toute requête vers votre domaine, même déclenchée depuis un site tiers. Un formulaire hostile peut donc soumettre /account/delete avec la session de la victime. La parade classique est le jeton synchronisé : un secret aléatoire stocké en session et exigé dans chaque requête mutative. Un site tiers ne peut ni le lire ni le deviner, donc pas le fournir.
<?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>L'injection SQL suit le même schéma : concaténer une valeur utilisateur dans une requête revient à laisser l'utilisateur écrire du SQL - ' OR 1=1 -- transforme un filtre en table entière. La solution n'est pas l'échappement mais la requête préparée : le texte de la requête (avec ses marqueurs :email) et les valeurs voyagent séparément jusqu'au moteur, qui ne peut donc jamais confondre données et code. Détail d'expert : avec MySQL, PDO émule les préparations par défaut en échappant côté client ; passez PDO::ATTR_EMULATE_PREPARES à false pour des préparations réellement natives.
<?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));