Iframe en HTML: qué es, atributos y por qué es la etiqueta más peligrosa si no sabes lo que haces

Un iframe no incrusta un vídeo: incrusta un documento entero con su propio código dentro del tuyo. Qué atributos importan de verdad y cómo cerrar los agujeros de seguridad que casi ningún tutorial menciona.

Cuadros enmarcados colgados en la pared de una galería, en blanco y negro
Tabla de contenidos

Copiar un <iframe> de YouTube y pegarlo en tu HTML es de las cosas más fáciles que se hacen en frontend. Por eso casi nadie se para a pensar en lo que acaba de meter en su página. Un iframe no es una imagen ni un bloque de texto: es un documento entero, con su propio origen, su propio JavaScript y sus propios permisos, incrustado dentro del tuyo. Bien usado, resuelve incrustaciones que de otra forma serían un infierno. Mal usado, es la etiqueta con la que un tercero ejecuta código en el contexto de tu web, la que hace que tu página cargue medio megabyte antes de que nadie pulse "play", y la que deja la puerta abierta a que embeban tu sitio para robar clics a tus usuarios.

Este artículo es el que me habría gustado encontrar la primera vez: qué es un iframe de verdad, qué atributos importan y cuáles son decorado, y sobre todo dónde está el peligro y cómo se cierra. La parte de seguridad, que es la que casi todos los tutoriales despachan con un "usa fuentes de confianza", es la que más espacio se lleva aquí, porque es donde se falla en producción.

¿Qué es un iframe y para qué sirve?

Un <iframe> (de inline frame, marco en línea) es un elemento HTML que incrusta otro documento HTML completo dentro del actual. La especificación lo llama un "contexto de navegación anidado": dentro de tu página abres, en la práctica, una ventana de navegador más pequeña que carga su propia URL, con su árbol DOM independiente, sus estilos, sus cookies y sus scripts.

Esa independencia es la clave de todo lo que viene después. El documento de dentro y el de fuera son dos mundos separados que solo pueden hablarse por canales muy concretos y muy vigilados. No es que el navegador sea quisquilloso: es que si el iframe pudiera leer y escribir libremente en tu página, cualquier vídeo incrustado podría leer las contraseñas que el usuario teclea en tu formulario.

Los usos legítimos son de sobra conocidos: vídeos de YouTube o Vimeo, mapas de Google Maps, pasarelas de pago que exigen aislamiento (los formularios de tarjeta de Stripe viven en un iframe precisamente para que tu web nunca toque el número de la tarjeta), widgets de terceros, previsualizaciones de documentos y entornos de código en vivo como los de la documentación de MDN. En todos ellos el patrón es el mismo: quieres mostrar contenido de otro origen sin darle acceso al tuyo.

Cómo insertar un iframe en HTML: la sintaxis mínima y por qué no basta

La versión que aparece en todas partes es esta:

<iframe src="https://ejemplo.com/pagina"></iframe>

Funciona, sí. Y es exactamente lo que no deberías dejar en producción. Le falta lo que hace que un iframe sea accesible, rápido y seguro. La versión que sí escribo es más parecida a esta:

<iframe
  src="https://ejemplo.com/widget"
  title="Widget de reservas de la clínica"
  width="600"
  height="400"
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"
  sandbox="allow-scripts allow-forms"></iframe>

Cada atributo de esa lista responde a un problema real. Voy uno a uno, porque la diferencia entre las dos versiones es la diferencia entre "funciona en mi pantalla" y "funciona para todos, no me cuesta ancho de banda y no me abre un agujero".

Los atributos que de verdad importan

src es la URL del documento incrustado. Nada que añadir, salvo un detalle: para YouTube conviene usar el dominio youtube-nocookie.com, que no planta cookies de seguimiento hasta que el usuario interactúa.

title es obligatorio y casi nadie lo pone. No es decorativo: es la etiqueta que anuncian los lectores de pantalla al llegar al iframe. Sin él, una persona que navega con tecnología asistiva tiene que entrar dentro del marco para averiguar qué contiene, y como advierte la documentación de MDN sobre el elemento, ese cambio de contexto "puede ser confuso y lento, especialmente en páginas con varios iframes". El valor debe describir el contenido de forma concisa: title="Vídeo: cómo montar la estantería", no title="iframe".

loading="lazy" difiere la carga del iframe hasta que se acerca al viewport. El valor por defecto es eager, que carga de inmediato. Un mapa o un vídeo al final de un artículo largo no tienen por qué descargarse cuando el usuario aún está leyendo el primer párrafo. Un matiz que la gente no conoce: el diferido solo actúa con JavaScript habilitado, como medida antiseguimiento.

width y height en el propio elemento reservan el hueco y evitan que la página dé un salto (layout shift) cuando el iframe termina de cargar. Aunque luego ajustes el tamaño por CSS, ponerlos como atributos le da al navegador la relación de aspecto desde el primer momento.

referrerpolicy controla qué información de referencia se envía al origen incrustado. El valor por defecto ya es sensato, strict-origin-when-cross-origin: manda la URL completa en peticiones del mismo origen, solo el origen entre orígenes distintos, y nada cuando se baja de HTTPS a HTTP. El que hay que evitar es unsafe-url, que filtra la ruta completa incluso a destinos menos seguros.

allow define la Permissions Policy del iframe: qué funcionalidades del navegador puede usar el documento incrustado (cámara, micrófono, autoplay, fullscreen, geolocalización). Es una lista blanca. Si no incluyes camera, el iframe no puede pedir la cámara aunque su código lo intente.

allowfullscreen permite que el iframe entre en pantalla completa. Es un atributo legado que hoy se redefine como allow="fullscreen"; lo verás en los embeds de YouTube por compatibilidad.

Una mano sostiene un móvil con el logotipo de una aplicación de vídeo en pantalla
Un vídeo embebido carga cientos de kilobytes antes de que nadie le dé al play. La fachada existe por eso.

Vídeo responsive sin hacks: el contenedor con aspect-ratio

Durante años, hacer que un vídeo incrustado se adaptara al ancho manteniendo la proporción exigió un truco incómodo: un contenedor con position: relative, un padding-bottom: 56.25% (el resultado de dividir 9 entre 16) para forzar la altura, y el iframe en position: absolute ocupándolo todo. Funcionaba, pero era código que nadie entendía a la primera.

Hoy no hace falta. La propiedad CSS aspect-ratio, disponible en todos los navegadores modernos, resuelve lo mismo de forma legible:

.video-responsive {
  aspect-ratio: 16 / 9;
  width: 100%;
}

.video-responsive iframe {
  width: 100%;
  height: 100%;
  border: 0;
}
<div class="video-responsive">
  <iframe
    src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
    title="Reproductor de vídeo"
    loading="lazy"
    allow="fullscreen; picture-in-picture"></iframe>
</div>

El contenedor mantiene la relación 16:9 sea cual sea el ancho, y el iframe lo rellena. Cinco líneas de CSS que cualquiera lee y entiende, sin porcentajes mágicos. Si necesitas otra proporción, cambias el aspect-ratio: 4 / 3, 1 / 1, lo que sea.

Por qué el iframe es la etiqueta más peligrosa del HTML

Aquí está el corazón del asunto. Un iframe mete en tu página código que tú no controlas. Y hay dos direcciones de peligro que conviene no confundir:

  1. El iframe que incrustas te ataca a ti (a tu página o a tus usuarios). Un widget de un tercero con JavaScript malicioso, o comprometido después de que lo integraras, corriendo dentro de tu web.
  2. Otra web te incrusta a ti para atacar a tus usuarios. Es el clickjacking: alguien pone tu página real dentro de un iframe invisible sobre su propio contenido, y el usuario cree que pulsa un botón inocente cuando en realidad está pulsando "Confirmar pago" en tu sitio.

Son problemas distintos con defensas distintas. Contra el primero, el atributo sandbox. Contra el segundo, las cabeceras X-Frame-Options y, mejor, Content-Security-Policy: frame-ancestors. El error de la mayoría de tutoriales es tratar "¿es seguro un iframe?" como una pregunta de sí o no. No lo es: depende de qué defensa aplicas contra cuál de los dos vectores.

sandbox: qué desactiva cada token y la combinación que anula la protección

El atributo sandbox aísla el documento incrustado. La regla mental es la contraria de lo que parece: sandbox a secas, sin ningún valor, aplica todas las restricciones. El iframe no puede ejecutar scripts, ni enviar formularios, ni abrir ventanas, ni navegar la página que lo contiene, y se trata como si viniera de un origen único que siempre falla la política del mismo origen. A partir de ahí, cada token que añades relaja una restricción concreta:

Token Qué permite al iframe
allow-scripts Ejecutar JavaScript (pero no abrir popups)
allow-forms Enviar formularios
allow-same-origin Conservar su origen real (acceso a sus cookies, storage)
allow-popups Abrir ventanas con window.open() o target="_blank"
allow-modals Usar alert(), confirm(), prompt(), print()
allow-downloads Descargar ficheros
allow-top-navigation Navegar la página de nivel superior
allow-popups-to-escape-sandbox Que las ventanas que abra no hereden el sandbox

La idea es empezar por lo más restrictivo y conceder solo lo imprescindible. Un widget que solo muestra información no necesita nada. Uno con un formulario necesita allow-forms y probablemente allow-scripts, y ahí ya conviene pararse.

Y ahora la parte que casi nadie explica y que MDN advierte en una nota fácil de pasar por alto. Si combinas allow-scripts y allow-same-origin a la vez, y el documento incrustado comparte origen con el tuyo, el sandbox deja de servir para nada. El motivo es demoledor en su simpleza: con esas dos capacidades juntas, el propio JavaScript del iframe puede alcanzar su elemento <iframe> en el DOM padre y eliminar el atributo sandbox. La documentación lo dice sin rodeos: queda "no más seguro que no usar el atributo sandbox en absoluto".

<!-- MAL: el sandbox no protege nada -->
<iframe src="/widget-mismo-origen" sandbox="allow-scripts allow-same-origin"></iframe>

Si de verdad necesitas ejecutar scripts de un tercero de forma aislada, sírvelo desde un origen distinto (un subdominio dedicado, sin cookies de sesión) y así allow-same-origin no le devuelve acceso a nada tuyo. Es la misma clase de trampa silenciosa que una comparación con NULL que nunca es verdadera: el código parece que protege, y no protege.

Clickjacking: cómo evitar que embeban tu web

Este es el vector inverso. Tú no incrustas a nadie; alguien te incrusta a ti. Pone tu página en un iframe transparente encima de un cebo ("Has ganado un premio, pulsa aquí") y engaña al usuario para que interactúe con tu sitio sin saberlo. La defensa consiste en decirle al navegador quién tiene permiso para meterte en un iframe.

Hay dos mecanismos y conviene saber cuál usar. El histórico es la cabecera HTTP X-Frame-Options, con dos valores válidos: DENY (nadie puede embeberte) y SAMEORIGIN (solo tu propio origen). Ojo con dos malentendidos habituales: el valor ALLOW-FROM está obsoleto y los navegadores modernos lo ignoran, y esta cabecera no funciona en una etiqueta <meta>, solo como cabecera HTTP real.

El mecanismo moderno, y el que MDN recomienda, es la directiva frame-ancestors de Content-Security-Policy. Es más expresiva y no arrastra el lastre de ALLOW-FROM:

Content-Security-Policy: frame-ancestors 'none';

'none' equivale a X-Frame-Options: DENY. Para permitir solo tu propio origen:

Content-Security-Policy: frame-ancestors 'self';

Y para autorizar orígenes concretos (por ejemplo, un panel interno que sí debe embeberte):

Content-Security-Policy: frame-ancestors 'self' https://panel.midominio.com;

Dos detalles que evitan sustos: frame-ancestors no hereda de default-src, así que aunque tengas default-src 'none', si no declaras frame-ancestors explícitamente cualquiera puede embeberte. Y, igual que X-Frame-Options, tampoco funciona en <meta>: tiene que ir como cabecera HTTP.

En la práctica, para máxima cobertura pongo las dos, con CSP como fuente de verdad y X-Frame-Options como red para navegadores viejos:

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self';

Si tu web maneja sesiones autenticadas o formularios que cambian estado y no tienes ninguna de estas cabeceras, tienes un problema abierto. No es teórico: es la configuración por defecto que deja vulnerable a media internet.

La política del mismo origen: qué puede y qué no puede hacer un iframe

Toda esta arquitectura se apoya en la política del mismo origen. Dos documentos comparten origen si coinciden en esquema, host y puerto. Sobre esa base:

  • Iframe del mismo origen que tu página: el padre y el hijo pueden leerse y manipularse mutuamente el DOM con iframe.contentWindow y window.parent. Máxima confianza, porque es tu propio código.
  • Iframe de origen distinto: el navegador bloquea casi todo. El padre no puede leer el DOM del iframe ni sus cookies, y el iframe no puede leer el DOM del padre. Lo poco que sí se permite entre orígenes son unas pocas acciones acotadas (cambiar location, redimensionar en ciertos casos) y, sobre todo, el paso de mensajes explícito.

Esa frontera es lo que hace que puedas incrustar un vídeo de YouTube sin que YouTube lea lo que el usuario escribe en tu formulario de contacto. No es cortesía del proveedor: es el navegador imponiéndolo. Cuando de verdad necesitas que el iframe y tu página se comuniquen a través de esa frontera, hay un único canal correcto.

postMessage: comunicación validando SIEMPRE el origin

window.postMessage() es la forma segura de que dos documentos de orígenes distintos intercambien datos. Y tiene dos reglas que, si te las saltas, convierten la solución en el propio agujero.

Al enviar, especifica un targetOrigin exacto. Nunca '*'. El segundo argumento de postMessage dice a qué origen se le permite recibir el mensaje. Si pones '*', cualquier web que en ese momento tenga tu documento embebido puede leer el contenido. Si el mensaje lleva algo sensible, lo acabas de regalar.

// Dentro del iframe: avisa al padre de su altura para autoajustarlo.
// targetOrigin exacto: solo midominio.com puede leer este mensaje.
window.parent.postMessage(
  { tipo: 'resize', altura: document.body.scrollHeight },
  'https://www.midominio.com'
);

Al recibir, valida SIEMPRE event.origin antes de tocar nada. Cualquier ventana puede enviarte un message. Si procesas event.data sin comprobar de dónde viene, estás ejecutando datos de un remitente desconocido:

window.addEventListener('message', (event) => {
  // 1. Comprueba el origen. Si no coincide, se descarta y punto.
  if (event.origin !== 'https://widget.midominio.com') return;

  // 2. Solo ahora es seguro usar los datos.
  const { tipo, altura } = event.data;
  if (tipo === 'resize') {
    document.querySelector('#mi-widget').style.height = `${altura}px`;
  }
});

Ese if (event.origin !== ...) return de la primera línea es toda la seguridad. Se omite constantemente porque "en local funciona igual", y es exactamente por donde entra el ataque.

Rendimiento: un embed de YouTube pesa cientos de kB antes del play

Un embed estándar de YouTube no es barato. Antes de que nadie pulse "play", ese iframe descarga el reproductor completo: según las mediciones de lite-youtube-embed de Paul Irish, alrededor de 1,2 MB y más de veinte peticiones de red. Pon tres vídeos en una página y le has cargado varios megabytes a un usuario que a lo mejor no reproduce ninguno.

La técnica que lo arregla se llama fachada (facade): en vez del iframe real, muestras una imagen ligera, la miniatura del vídeo, unos pocos kB, con un botón de "play" encima. Solo cuando el usuario hace clic, sustituyes la fachada por el iframe de verdad. El coste pesado se paga únicamente si hay intención de ver el vídeo.

<div class="yt-facade" data-id="VIDEO_ID">
  <button type="button" aria-label="Reproducir el vídeo">
    <img src="https://i.ytimg.com/vi/VIDEO_ID/hqdefault.jpg"
         alt="" width="480" height="360" loading="lazy">
  </button>
</div>
document.querySelectorAll('.yt-facade').forEach((facade) => {
  facade.addEventListener('click', () => {
    const iframe = document.createElement('iframe');
    iframe.src = `https://www.youtube-nocookie.com/embed/${facade.dataset.id}?autoplay=1`;
    iframe.title = 'Reproductor de vídeo de YouTube';
    iframe.allow = 'autoplay; encrypted-media; fullscreen; picture-in-picture';
    iframe.width = 560;
    iframe.height = 315;
    facade.replaceChildren(iframe);
  }, { once: true });
});

Existen componentes ya hechos si no quieres mantener el tuyo (el propio lite-youtube-embed ronda los 27 kB frente al megabyte largo del embed nativo), pero el principio es lo que importa: no pagues por adelantado un recurso que la mayoría de visitas no van a usar. Combinado con loading="lazy", es la diferencia entre una página que carga rápido y una que arrastra el reproductor de un vídeo que nadie ve.

Cuándo NO usar un iframe

Un artículo honesto sobre iframes tiene que incluir esta sección, porque el mejor iframe es a menudo el que no pones. Evítalo cuando:

  • Puedes hacerlo con un componente nativo. Para mostrar contenido tuyo, un componente del framework o un Web Component es más ligero, más accesible y controlas su ciclo de vida. El iframe es para contenido de otro origen; usarlo dentro de tu propia app suele ser pereza arquitectónica.
  • Solo quieres incrustar un tuit, un vídeo o un post. Muchas plataformas ofrecen oEmbed: pides una URL y te devuelven el HTML de la incrustación, a menudo sin el peso de un iframe completo. La fachada del apartado anterior es una versión artesanal de esta misma idea.
  • El SEO de ese contenido importa. Lo que vive dentro de un iframe pertenece a otro documento; los buscadores lo atribuyen a su origen, no al tuyo. Si necesitas que ese contenido posicione en tu página, no lo metas en un iframe.
  • Necesitas que el contenido interactúe de verdad con tu página. Si te ves peleando con postMessage para sincronizar estado en ambas direcciones, probablemente el iframe es la herramienta equivocada y lo que quieres es una integración por API.

El iframe es una gran herramienta para un problema muy concreto: mostrar el documento de un tercero sin darle las llaves de tu casa. Para casi todo lo demás, hay opciones mejores. Y cuando sí lo uses, que no sea la versión de una línea: title para quien no ve la pantalla, sandbox bien pensado, loading="lazy" para no malgastar ancho de banda y frame-ancestors en tu propia web para que nadie te embeba con malas intenciones. Si estás integrando iframes de terceros en un proyecto y quieres una segunda opinión sobre la configuración de seguridad, hablamos de tu proyecto sin compromiso.

Preguntas frecuentes

¿Qué es un iframe y para qué sirve?

Un iframe (inline frame) es un elemento HTML que incrusta otro documento HTML completo dentro del actual, creando lo que la especificación llama un contexto de navegación anidado: una especie de mini-navegador con su propio DOM, sus scripts y sus cookies. Sirve para mostrar contenido de otro origen (vídeos de YouTube, mapas, pasarelas de pago, widgets) sin darle acceso a tu página. Esa separación es precisamente lo que impide que el contenido incrustado lea lo que el usuario escribe en tu web.

¿Cómo pongo un iframe en una página web?

La sintaxis mínima es <iframe src="https://url"></iframe>, pero no la dejes así en producción. Añade siempre title (para lectores de pantalla), loading="lazy" (para no cargarlo hasta que se acerque a la pantalla), width y height (para reservar el hueco y evitar saltos de maquetación) y, si incrustas contenido de terceros, un atributo sandbox con solo los permisos imprescindibles. Esa versión completa es la diferencia entre algo que funciona en tu pantalla y algo que funciona para todos sin abrir agujeros.

¿Es seguro utilizar iframes?

Depende de cómo lo configures, y no es una pregunta de sí o no. Hay dos riesgos distintos. El primero: el iframe que incrustas podría atacarte, y contra eso se usa el atributo sandbox concediendo solo los permisos mínimos. El segundo: otra web podría incrustarte a ti para engañar a tus usuarios (clickjacking), y contra eso se usan las cabeceras Content-Security-Policy: frame-ancestors y X-Frame-Options. El consejo de "solo usa fuentes de confianza" es insuficiente: una fuente de confianza puede quedar comprometida, y no te protege del clickjacking en absoluto.

¿Por qué sandbox con allow-scripts y allow-same-origin juntos no protege?

Porque, cuando el documento incrustado comparte origen con el tuyo, esas dos capacidades juntas le permiten a su propio JavaScript alcanzar el elemento iframe en el DOM padre y eliminar el atributo sandbox por completo. MDN lo advierte: la combinación queda "no más segura que no usar sandbox en absoluto". Si necesitas ejecutar scripts de terceros aislados, sírvelos desde un origen distinto (un subdominio sin cookies de sesión) para que allow-same-origin no le devuelva acceso a nada tuyo.

¿Cómo evito que mi web se cargue dentro de un iframe de otro (clickjacking)?

Con cabeceras HTTP que indiquen quién puede embeberte. La moderna y recomendada es Content-Security-Policy: frame-ancestors 'self' (solo tu origen) o 'none' (nadie). La histórica es X-Frame-Options: SAMEORIGIN o DENY. Lo ideal es poner ambas, con CSP como fuente de verdad y X-Frame-Options como respaldo para navegadores antiguos. Ninguna de las dos funciona en una etiqueta meta: tienen que ir como cabecera HTTP real. Y frame-ancestors no hereda de default-src, así que hay que declararla explícitamente.

¿Por qué carga tan lento mi iframe de YouTube?

Porque un embed estándar de YouTube descarga el reproductor completo antes de que nadie pulse play: alrededor de 1,2 MB y más de veinte peticiones de red, según las mediciones de lite-youtube-embed. Con varios vídeos en la página, son varios megabytes de coste para visitas que quizá no reproduzcan nada. La solución es la técnica de la fachada: mostrar la miniatura ligera del vídeo con un botón de play y sustituirla por el iframe real solo al hacer clic. Así el coste pesado se paga únicamente si hay intención de ver el vídeo.

¿Te ha servido?

Si tienes un problema parecido y quieres una opinión técnica, cuéntamelo. Te respondo en menos de 24 h laborables.

WhatsApp