XSS DOM (CWE-79) : données attaquant injectées dans un sink DOM dangereux côté client, sans transiter par le serveur — invisible aux scanners DAST classiques.
TL;DR
location.hash, postMessage) vers un sink (innerHTML, eval)new Function() dans le rendu de polices exécutait du JS arbitraireLe XSS basé sur le DOM (CWE-79, Type-0 XSS dans la taxonomie OWASP) est une variante où la vulnérabilité existe entièrement dans le JavaScript côté client. La réponse du serveur est légitime — aucune donnée malveillante ne passe par la couche HTTP. À la place, le code côté client lit des données contrôlées par l'attaquant depuis une source DOM et les écrit dans un sink dangereux sans sanitization. Le script s'exécute dans le navigateur de la victime sous l'origine de l'application.
Cela rend le XSS DOM fondamentalement différent des variantes réfléchie et stockée : les WAF côté serveur ne voient aucun trafic d'attaque, et tout outil DAST qui examine uniquement les réponses HTTP ne le détectera pas. La détection nécessite une analyse JavaScript dynamique — un navigateur sans tête instrumentant l'exécution réelle, traçant les données de la source au sink.
Le flux de données source → sink est le modèle central de l'analyse du XSS DOM.
Sources — entrées contrôlables par l'attaquant que JavaScript peut lire :
| Source | Notes |
|---|---|
location.search | Chaîne de requête URL — la plus courante |
location.hash | Identifiant de fragment — jamais envoyé au serveur, invisible pour les WAF |
document.referrer | Contrôlé par l'attaquant via navigation |
window.name | Persiste à travers les navigations ; contourne la même origine |
Données postMessage | Cross-origin ; ne nécessite pas de validation event.origin |
localStorage / sessionStorage | Données précédemment contaminées |
document.cookie | Injection de cookie via CRLF ou sous-domaine |
| Messages WebSocket | Flux de données en temps réel |
Sinks — opérations d'écriture DOM dangereuses pouvant produire une exécution de script :
| Sink | Danger | Notes |
|---|---|---|
element.innerHTML | Critique | Exécute les gestionnaires d'événements et <script> |
document.write() | Critique | Casse le contexte du parseur |
eval() | Critique | Exécution directe de code |
setTimeout(chaîne, ...) | Critique | L'argument chaîne est évalué comme JS |
setInterval(chaîne, ...) | Critique | Même chose |
Function(chaîne)() | Critique | Utilisé dans PDF.js (CVE-2024-4367) |
element.insertAdjacentHTML() | Critique | Équivalent à innerHTML |
location.href = "javascript:..." | Élevé | Redirection vers URI JS |
element.setAttribute("onerror", ...) | Élevé | Injection de gestionnaire d'événements |
element.src / element.href | Moyen | URI data: / javascript: |
Le schéma classique de XSS DOM lit depuis location.search et écrit dans innerHTML :
// VULNÉRABLE — de la source au sink sans sanitization
const params = new URLSearchParams(location.search);
const name = params.get('name');
document.getElementById('greeting').innerHTML = 'Bonjour, ' + name;
// Payload : ?name=<img src=x onerror=alert(document.domain)>
// Le navigateur insère le HTML brut, exécute le gestionnaire onerrorLe serveur renvoie une réponse 200 normale. Toute la vulnérabilité existe dans deux lignes de JavaScript côté client.
Variante postMessage — exploitable depuis n'importe quelle page cross-origin :
// VULNÉRABLE — pas de validation d'origine
window.addEventListener('message', function(e) {
// e.origin non vérifié — n'importe quelle origine peut envoyer
document.getElementById('content').innerHTML = e.data; // sink
});// Page de l'attaquant (n'importe quelle origine) :
const target = window.open('https://victim.com/app');
setTimeout(() => {
target.postMessage('<img src=x onerror=alert(document.domain)>', '*');
}, 1000);CVE-2024-49038 (Microsoft Copilot Studio, CVSS 9.3) a exploité un schéma postMessage/injection DOM dans un service AI cloud, permettant une escalade de privilèges cross-tenant.
La prototype pollution définit des propriétés arbitraires sur Object.prototype via des paramètres de requête craftés. Quand le code de l'application ou une bibliothèque lit une propriété indéfinie d'un objet quelconque, elle remonte au prototype pollué.
// Étape 1 : Prototype pollution via URL
// ?__proto__[transport_url]=data:,alert(document.domain);
// Définit : Object.prototype.transport_url = "data:,alert(1)"
// Étape 2 : Le gadget jQuery lit la propriété
$.ajax({ url: '/api', jsonp: callback });
// jQuery lit en interne options.transport_url — indéfini sur l'objet options
// Remonte vers Object.prototype.transport_url → le payload de l'attaquant s'exécuteCVE-2026-41238 démontre que DOMPurify lui-même est un gadget de prototype pollution : polluer Object.prototype.tagNameCheck avec une regex permissive amène DOMPurify à laisser passer des éléments arbitraires dans la sanitization, convertissant une vulnérabilité de prototype pollution n'importe où dans l'application en bypass XSS complet.
Outils de détection : Burp Suite DOM Invader (scanner de prototype pollution intégré), l'extension Chrome PPScan, et le dépôt GitHub client-side-prototype-pollution (liste de gadgets maintenue par BlackFan).
Le DOM clobbering utilise des éléments HTML nommés — injectés via injection HTML dans des contextes où <script> est bloqué — pour masquer des variables JavaScript globales.
<!-- Clobber à un niveau : window.x devient un HTMLElement -->
<a id="x" href="https://attacker.com/malicious.js"></a>
<!-- Clobber à deux niveaux : window.config.cdn devient une chaîne via l'attribut href d'une ancre -->
<a id="config"><a id="config" name="cdn" href="https://attacker.com">Quand le code de l'application exécute var url = window.config.cdn || '/assets/', la valeur clobbered config.cdn est https://attacker.com — contrôlée par l'attaquant.
CVE-2024-7524 — Contournement CSP strict-dynamic de Firefox : Le shim ETP du SDK Facebook lit document.currentScript.src sans valider le tagName. Injecter <img name="currentScript" src="data:,alert(document.domain)"> amène le shim à charger l'URI data: comme script enfant de confiance sous strict-dynamic, contournant entièrement le CSP.
Le DOM clobbering escalade l'injection HTML — où <script> est bloqué — en exécution complète de script. Une application qui sanitise <script> mais autorise les attributs id et name sur les éléments <a> et <form> reste vulnérable aux chaînes de XSS par DOM clobbering.
Le pipeline de rendu de polices de PDF.js compile les instructions de glyphes dans des corps new Function(). Un tableau FontMatrix contrôlable dans les métadonnées PDF — inséré brut dans le corps de la fonction sans validation de type — ferme l'appel légitime et ajoute du JavaScript arbitraire.
# Définition de police PDF malveillante
/FontMatrix [1 2 3 4 5 (0\); alert\('CVE-2024-4367'\))]CVSS 8.8 ÉLEVÉ. Affectait toutes les versions de Firefox < 126 et toutes les applications web/Electron embarquant pdfjs-dist (~2,7 millions de téléchargements npm hebdomadaires). Le sink new Function() est dangereux précisément parce qu'il contourne entièrement l'étape de parsing HTML — aucune injection de balise nécessaire.
location.search, location.hash, document.referrer, window.name, addEventListener('message', ...)postMessage avec un payload XSS depuis une origine contrôlée par l'attaquant// Test manuel rapide — coller dans la console du navigateur
// Vérifie si location.hash atteint innerHTML
const hash = decodeURIComponent(location.hash.slice(1));
// Vérifier toutes les assignations innerHTML manuellement dans l'onglet Sources des DevToolsBreachVex détecte le XSS DOM en utilisant des sessions de navigateur sans tête instrumentées qui accrochent tous les sinks dangereux au chargement de la page, tracent l'arrivée du canary à chaque sink avec des traces de pile complètes, et confirment l'exécution en interceptant la boîte de dialogue résultante.
Les Trusted Types (appliquées sur YouTube, Microsoft 365 et Stripe) empêchent les sinks DOM d'accepter des chaînes brutes :
// En-tête CSP pour appliquer :
// Content-Security-Policy: require-trusted-types-for 'script'
// Politique sûre — DOMPurify sanitise avant assignation
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
});
element.innerHTML = policy.createHTML(userInput); // OK — passe par le sanitizer
element.innerHTML = userInput; // TypeError en mode d'applicationCela prévient le XSS DOM même quand un flux source-vers-sink existe — le navigateur lance une exception avant que le sink s'exécute.
Remplacer les sinks dangereux par des équivalents sûrs :
// VULNÉRABLE : innerHTML avec données utilisateur
element.innerHTML = userInput;
// SÛR : textContent pour le texte brut (ne rend jamais le HTML)
element.textContent = userInput;
// SÛR : construction DOM structurée (pas de parsing HTML)
const p = document.createElement('p');
p.textContent = userInput;
container.appendChild(p);
// SÛR : DOMPurify + Trusted Types quand le HTML est nécessaire
element.innerHTML = trustedTypesPolicy.createHTML(DOMPurify.sanitize(userInput));// VULNÉRABLE — pas de vérification d'origine
window.addEventListener('message', function(e) {
element.innerHTML = e.data;
});
// SÛR — liste d'autorisation stricte d'origines attendues
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://checkout.example.com']);
window.addEventListener('message', function(e) {
if (!ALLOWED_ORIGINS.has(e.origin)) return; // rejeter les origines inconnues
element.textContent = e.data; // utiliser un sink sûr
});// Geler Object.prototype pour empêcher la pollution
Object.freeze(Object.prototype);
// Ou : utiliser Object.create(null) pour les objets de configuration (pas de chaîne prototype)
const config = Object.create(null);
config.timeout = 5000;
// La pollution de Object.prototype ne peut pas atteindre les propriétés de configLe XSS basé sur le DOM se produit entièrement dans le JavaScript côté client — aucune donnée malveillante n'atteint le serveur et la réponse HTTP du serveur est propre. Le XSS réfléchi nécessite que le serveur renvoie le payload dans sa réponse. Le XSS DOM est invisible pour les WAF côté serveur et la plupart des outils DAST qui examinent les réponses HTTP.
Les sources sont des entrées contrôlables par l'attaquant lues par JavaScript : location.search, location.hash, document.referrer, window.name, données d'événement postMessage, localStorage, sessionStorage, document.cookie, messages WebSocket et IndexedDB.
Les sinks sont des opérations d'écriture DOM dangereuses : innerHTML, outerHTML, document.write(), eval(), setTimeout(chaîne), setInterval(chaîne), Function(chaîne)(), insertAdjacentHTML(), location.href avec URI javascript:, element.src/href pour les URI data:, et la méthode .html() de jQuery.
La prototype pollution permet à un attaquant de définir des propriétés arbitraires sur Object.prototype via des paramètres de requête comme ?__proto__[key]=val. Quand le code de l'application ou une bibliothèque lit une propriété indéfinie d'un objet, elle remonte au prototype pollué, que l'attaquant a défini comme payload XSS. CVE-2026-41238 montre que DOMPurify lui-même est une cible de gadget.
Le DOM clobbering écrase des variables JavaScript globales avec des éléments HTML nommés. Une injection HTML d'un élément ancre avec l'id 'x' crée window.x comme HTMLElement, masquant toute variable JavaScript nommée x. Cela peut escalader l'injection HTML (où les balises script sont bloquées) en XSS complet en corrompant une variable utilisée comme URL de script ou source innerHTML.
Le XSS DOM nécessite une analyse dynamique : Burp Suite DOM Invader (instrumente Chrome pour tracer le flux de données source vers sink), automatisation de navigateur sans tête avec suivi de contamination (Playwright), ou analyse sémantique CodeQL. Les scanners côté serveur examinant les réponses HTTP ne peuvent pas le détecter.
Oui — les Trusted Types imposent que toutes les assignations aux sinks DOM passent par une politique définie par le développeur. Quand require-trusted-types-for 'script' est dans le CSP, assigner une chaîne brute à innerHTML lance une TypeError. Cela élimine le XSS DOM au niveau de la plateforme pour les applications conformes. Chrome/Edge les appliquent ; le support Firefox est en cours.
CVE-2024-4367 a exploité le pipeline de rendu de polices de PDF.js, qui compilait les instructions de glyphes dans des corps new Function(). Un tableau FontMatrix contrôlable dans les métadonnées PDF injectait du JavaScript arbitraire qui s'exécutait sur le domaine embarqueur. Avec ~2,7 millions de téléchargements npm hebdomadaires, cela affectait toutes les applications web et Electron embarquant PDF.js.