Cómo comunicar tu página con un iframe de forma segura usando postMessage en JavaScript

¡Qué onda, devs! Si alguna vez has intentado meter una página web dentro de otra usando un <iframe>, probablemente te has topado con problemas de seguridad o con la gran pregunta: ¿cómo hago para que mi página principal y el iframe se hablen sin poner en riesgo mi aplicación?

En la industria del desarrollo web, proteger la información de nuestros usuarios es primordial. Por ejemplo, en el mundo del video digital e interactivo, antes se solía dar acceso total al navegador a anuncios o plugins externos con tecnologías antiguas como VPAID. Sin embargo, esto era un peligro gigantesco porque un script malicioso podía modificar el DOM de la página principal o robar cookies. Hoy en día, estándares modernos como SIMID utilizan un concepto mucho más seguro: cargar contenido en un iframe totalmente aislado (sandboxed) y comunicarse únicamente mediante mensajes controlados con JavaScript.

En este tutorial aprenderás desde cero cómo integrar y comunicar un iframe seguro utilizando el atributo sandbox de HTML y la API postMessage de JavaScript.


¿Qué es un iframe sandboxed y por qué lo necesitas?

Un <iframe> (Inline Frame) te permite incrustar un documento HTML dentro de otro. Por defecto, si el iframe proviene del mismo dominio, tiene acceso completo al documento padre. Si viene de un dominio distinto, el navegador bloquea el acceso directo por la política del mismo origen (Same-Origin Policy).

Para reforzar la seguridad, HTML5 introdujo el atributo sandbox. Al colocar sandbox en un iframe, el navegador le quita todos los privilegios por defecto:

  • Deshabilita la ejecución de scripts en el iframe.
  • Impide el envío de formularios.
  • Bloquea el acceso al almacenamiento local (localStorage o cookies).
  • Evita que la página embebida redireccione o modifique la ventana principal.

Si queremos que nuestro iframe ejecute JavaScript de forma controlada pero sin permitirle manipular nuestra página principal, podemos darle permisos específicos agregando valores como sandbox="allow-scripts".


El puente de comunicación: window.postMessage

Dado que un iframe aislado no puede tocar directamente las funciones o variables de la página principal (ni al revés), necesitamos un canal de comunicación seguro. Aquí es donde entra la API postMessage de JavaScript.

postMessage permite que dos ventanas o marcos diferentes se envíen mensajes de texto o datos estructurados de manera asíncrona. Lo mejor de todo es que permite verificar siempre la identidad del remitente antes de procesar cualquier información.


Paso a paso: Creando una comunicación bidirecol segura

Vamos a construir un ejemplo funcional desde cero con dos archivos HTML: index.html (nuestro sitio principal) e interactive.html (el contenido interactivo que irá dentro del iframe).

Paso 1: Crear el contenido dentro del iframe (interactive.html)

Este archivo representa la aplicación embebida (por ejemplo, un widget de encuestas o un anuncio interactivo).

<!-- interactive.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Widget Interactivo</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background-color: #212529;
      color: #ffffff;
      padding: 20px;
      text-align: center;
    }
    button {
      background-color: #0d6efd;
      color: white;
      border: none;
      padding: 10px 15px;
      border-radius: 5px;
      cursor: pointer;
      font-size: 16px;
    }
    button:hover {
      background-color: #0b5ed7;
    }
  </style>
</head>
<body>
  <h3>¡Hola desde el Iframe!</h3>
  <button id="btnEnviar">Enviar evento a la página principal</button>

  <script>
    const boton = document.getElementById('btnEnviar');

    boton.addEventListener('click', () => {
      // Enviamos un mensaje a la ventana padre
      // El segundo argumento es el 'targetOrigin'. Usa '*' solo en pruebas locales.
      window.parent.postMessage({
        type: 'USER_ACTION',
        message: 'El usuario hizo clic en el botón del iframe'
      }, '*'); 
    });

    // Escuchar respuestas enviadas desde la página principal
    window.addEventListener('message', (event) => {
      console.log('Respuesta recibida en el iframe:', event.data);
    });
  </script>
</body>
</html>

Paso 2: Crear la página principal (index.html)

Esta página cargará el iframe con restricciones de seguridad y escuchará los mensajes que este envíe.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Página Principal - 8devmx</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      background-color: #121212;
      color: #e0e0e0;
    }
    iframe {
      width: 100%;
      height: 220px;
      border: 2px solid #0d6efd;
      border-radius: 8px;
    }
    #log {
      background-color: #1e1e1e;
      padding: 15px;
      border-radius: 5px;
      margin-top: 15px;
      font-family: monospace;
      border-left: 4px solid #0d6efd;
    }
  </style>
</head>
<body>
  <h1>Comunicación entre ventanas con postMessage</h1>
  <p>A continuación se muestra un iframe cargado en un entorno de pruebas aislado:</p>

  <!-- Aplicamos el aislamiento con sandbox permitiendo solo scripts -->
  <iframe id="miIframe" src="interactive.html" sandbox="allow-scripts"></iframe>

  <h3>Historial de eventos recibidos:</h3>
  <div id="log">Esperando mensajes...</div>

  <script>
    const iframe = document.getElementById('miIframe');
    const logDiv = document.getElementById('log');

    // Escuchar el evento 'message' globalmente
    window.addEventListener('message', (event) => {
      // BUENA PRÁCTICA: Validar el origen en producción
      // if (event.origin !== 'https://tusitio.com') return;

      const data = event.data;

      if (data && data.type === 'USER_ACTION') {
        logDiv.textContent = `[${new Date().toLocaleTimeString()}] Recibido: "${data.message}"`;
        
        // Opcional: Responderle al iframe confirmando la recepción
        event.source.postMessage({
          type: 'ACK',
          status: 'Mensaje procesado con éxito'
        }, event.origin || '*');
      }
    });
  </script>
</body>
</html>

Errores comunes al trabajar con postMessage e iframes

  1. Usar '*' como destino en producción: Al usar postMessage(data, '*'), le permites a cualquier dominio que contenga o envuelva el iframe leer la información. En producción siempre especifica la URL de destino explícita como https://mi-dominio.com.
  2. No validar event.origin: Si no compruebas el origen del mensaje recibido en el receptor, cualquier script malicioso dentro del navegador podría enviarle datos falsos a tu sitio.
  3. Intentar acceder al DOM directamente: Si intentas usar window.parent.document desde un iframe en aislamiento, la consola lanzará un error de seguridad. La comunicación debe hacerse exclusivamente enviando y recibiendo mensajes.
  4. Olvidar la regla allow-scripts: Si colocas únicamente el atributo sandbox vacío (<iframe sandbox>), el iframe bloqueará todo JavaScript y postMessage no podrá ejecutarse dentro de él.

Buenas prácticas de seguridad

  • Principio de menor privilegio: Otorga únicamente los permisos estrictamente necesarios en la propiedad sandbox. Evita mezclar allow-scripts y allow-same-origin en un mismo iframe no confiable, ya que esa combinación permite que el script embebido remueva sus propias restricciones.
  • Validación de datos: Trata todos los datos recibidos en event.data como inseguros. No utilices nunca innerHTML para pintar textos recibidos del iframe sin antes haberlos limpiado sanitizadamente para evitar ataques XSS.
  • Mensajes estructurados: Define un formato estándar para los datos que transmites, usando objetos con propiedades bien definidas como type y payload.

Ejercicio práctico: ¡Abre tu editor de código!

El Reto: Modifica el archivo interactive.html para agregar un contador de clics. Cada vez que el usuario presione el botón en el iframe, debes incrementar una variable interna y enviar la cantidad total actualizada a la página principal index.html.

Pistas para resolverlo:

  1. Crea una variable let totalClics = 0; en el script del iframe.
  2. Suma 1 a la variable cada vez que ocurra el evento click del botón.
  3. Modifica el objeto enviado en postMessage agregando la propiedad clics: totalClics.
  4. En la página principal, muestra el conteo en la pantalla dentro de la caja de registros.

¡Pon a prueba el código localmente en tu navegador y verás qué fácil es controlar frames externos sin comprometer tu aplicación!


Fuente original

Este artículo toma como inspiración las técnicas de aislamiento web expuestas en la evolución de reproductores interactivos en video (de VPAID a SIMID). Puedes checar la publicación original en inglés escrita por Alek M: VPAID’s Replacement Has Been Shipping for Years. Most Tags Still Fail SIMID.