Qu'est-ce qu'une injection XSS ?
L’injection XSS (Cross-Site Scripting) est une faille qui survient lorsqu’une application web affiche les données fournies par un utilisateur (depuis un formulaire) sans les sécuriser correctement. Dans ce cas, le navigateur peut exécuter des scripts malveillants généralement en JavaScript injectés par un attaquant (depuis le même formulaire), ce qui ne devrait jamais arriver.
Lorsqu'un attaquant exécute du code JavaScript malveillant dans le navigateur d'un utilisateur, il peut par exemple : - Lire ces cookies (notamment les cookies de session). - Changer l’affichage de la page pour le tromper. - Ou encore Rediriger l'utilisateur vers des sites malveillants.
On classe généralement le XSS en 3 catégories ; XSS Stocké (Stored), XSS Réfléchi (Reflected) et XSS DOM (DOM-based). Dans ce cours nous étudierons seulement le XSS Stocké.
XSS Stocké
Une attaque XSS est dite “stockée” lorsque le code malveillant injecté est enregistré sur le serveur et exécuté lors d’un affichage ultérieur. Imaginons la page web suivante :
// index.html
<!DOCTYPE html>
<body>
<h1>Poster un commentaire</h1>
// Formulaire pour rentrer un commentaire
<form method="post" action="submit.php">
<label>Message<br>
<textarea name="content" rows="5" cols="60" required></textarea>
</label><br>
<button type="submit">Envoyer</button>
</form>
<p>Après envoi, vous serez redirigé vers la page qui affiche tous les commentaires.</p>
</body>
</html>
Ici l'utilisateur peut rentrer un commentaire qui sera stocké dans une base de données, puis réafiché sur une autre page avec tous les autres commentaires des utilisateurs (déjà postés). Chaque utilisateur qui visite la page verra donc son nouveau commentaire et celui de tous les autres.
Examinons maintenant le code php qui va enregistrer le commentaire posté dans la base de données :
// submit.php
<?php
// Connexion à la base de données DB_test
$pdo = new PDO('mysql:host=127.0.0.1;dbname=DB_test;charset=utf8mb4', 'root', 'p@ssword');
// Récupération des données envoyées depuis le formulaire
$content = $_POST['content'] ?? ''; // Récupère la valeur du champ <textarea name="content">
// Insertion du commentaire dans la base de données (dans la table 'comments')
$stmt = $pdo->prepare("INSERT INTO comments (content) VALUES (:content)");
$stmt->execute([
':content' => $content
]);
// Redirection HTTP (vers comments.php)
header('Location: comments.php');
exit;
?>
Le fichier submit.php permet de récupérer le commentaire envoyé par l'utilisateur, le stocker dans la base de données et rediriger l'utilisateur vers une autre page (comments.php). Ce qu'il faut retenir ici c'est que ce fichier permet de stocker le commentaire de l'utilisateur dans la base de données, tel qu'il a été envoyé.
Examinons maintenant la vulnérabilité dans le code php qui va permettre d'afficher les commentaires, stockés dans la base de données, aux utilisateurs (Une attaque XSS s’exécute dans le navigateur de l’utilisateur, au moment où le contenu s’affiche) :
// comments.php
<?php
// Connexion à la base de données DB_test
$pdo = new PDO('mysql:host=127.0.0.1;dbname=DB_test;charset=utf8mb4', 'root', 'p@ssword');
// Récupération des commentaires stockés dans la base de données
$comments = $pdo->query("SELECT content FROM comments ORDER BY id DESC")->fetchAll();
//Fin du php côté serveur
?>
//Début du HTML pour afficher les commentaires - donc possibilité de faille XSS !
<!DOCTYPE html>
<body>
<h1>Commentaires</h1>
// Boucle d'affichage des commentaires - le php affiche tous les commentaires stockés dans la table 'comments'
<?php foreach ($comments as $c): ?>
// Faille XSS ici ! Le contenu est affiché tel quel...
<p><?php echo $c['content']; ?></p>
// Fin de la boucle
<?php endforeach; ?>
</body>
</html>
Ici, ce code permet d'afficher à l'utilisateur son commentaire et tous les autres, entrés sur le formulaire.
On a pu voir que dans le code précédent (submit.php) que le commentaire de l'utilisateur était stocké tel quel, c'est à dire que si l'utilisateur rentre une chaîne de caractère comme 'Hello', ce code (comments.php) va donc afficher le commentaire tel quel, sans le modifier, à savoir donc en chaîne de caractères. On dit aussi que 'le commentaire est affiché sans échappement'. Jusque là, aucun problème.
Mais imaginons maintenant que l'utilisateur soit malveillant et ne rentre pas une simple chaîne de caractères mais un script JavaScript. Que se passera t'il ?
Le fichier submit.php va donc stocker le commentaire tel quel, donc du texte contenant du code JavaScript (<script>alert('Bonjour')</script>). Et le fichier comments.php va aussi l'afficher tel quel à cause de la commande 'echo $c['content']; ?'. La faille est là ! La faille XSS se fait au moment de l'affichage.
Comme le commentaire est affiché sans être échappé (≈ sans être transformé), PHP l’envoie tel quel au navigateur. Le navigateur interprète alors ce contenu comme du HTML normal. En rencontrant la balise <script>, il exécute automatiquement le code JavaScript qu’elle contient, ce qui provoque l’attaque XSS.
Voici la nouvelle page de commentaire que vont voir les utilisateurs, après avoir posté leur commentaire :
Le script a bien été exécuté, l'attaque XSS est faite. Et comme la page de commentaires affichent tous les commentaires stockés de tous les utilisateurs, tous les utilisateurs verront maintenant ce nouveau commentaire stockés : l'exécution du script JavaScript.
Le script ici n'est pas tellement dangereux. Il affiche juste 'Bonjour' sur la page de chaque utilisateur. Mais si ce simple script est passé, tout autre script malveillant peut s'exécuter. Comme un script pour voler les cookies de session, rediriger l'utilisateur vers un faux formulaire de connexion pour récupérer son mot de passe ou encore espionner l'utilisateur en enregistrant tout ce qu'il tape...
Comment se protéger d'une injection XSS ?
Comme toutes les injections, le problème vient de la manière dont on traite les données envoyées par l'utilisateur. On a pu voir ici que les commentaires étaient affichés sans échappement. Il faut donc écrire un code en php pour afficher les données avec échappement.
// comments.php
// Boucle d'affichage des commentaires
<?php foreach ($comments as $c): ?>
// Afficher les commentaires avec échappement
<p><?= htmlspecialchars($c['content'], ENT_QUOTES, 'UTF-8'); ?></p>
// Fin de la boucle
<?php endforeach; ?>
Ici, le 'htmlspecialchars' transforme les caractères spéciaux HTML en texte. Donc la balise <script> devient '<script>' (C'est l'encodage de HTML pour le symbole des balises, '<>' ). Tous scripts JavaScript sera donc maintenant affiché comme une chaîne de caractères.
Le script n'est plus exécuté il est bien traité comme une chaîne de caractères et donc affiché comme un simple commentaire. L'attaque XSS ne fonctionne plus !
Moralité : en cybersécurité, on ne fait jamais confiance aux données utilisateur.