iframe «ha rechazado la conexión»: arreglar X-Frame-Options

Punto clave

«Ha rechazado la conexión» dentro de un iframe no es un error de red. El sitio incrustado envió una cabecera X-Frame-Options o CSP frame-ancestors que le indica a tu navegador que no lo muestre dentro de otra página. La solución está en el servidor del sitio incrustado, nunca en tu etiqueta iframe: mantén SAMEORIGIN en todo el sitio y abre una sola ruta de inserción con frame-ancestors *.

Compruébelo ahora

X-Frame-Options explicado: un iframe que muestra «ha rechazado la conexión» junto a la solución — SAMEORIGIN en todo el sitio, CSP frame-ancestors * en la ruta de inserción
X-Frame-Options explicado: un iframe que muestra «ha rechazado la conexión» junto a la solución — SAMEORIGIN en todo el sitio, CSP frame-ancestors * en la ruta de inserción
Empiece a leer
  1. Qué significa realmente «ha rechazado la conexión» dentro de un iframe
  2. Cómo funciona X-Frame-Options
  3. DENY o SAMEORIGIN: ¿cuál enviar?
  4. ALLOW-FROM está muerto: no lo uses
  5. frame-ancestors: la directiva CSP que lo reemplaza
  6. Qué pasa cuando están las dos cabeceras
  7. Paso 1: encuentra la cabecera que te bloquea
  8. Paso 2: decide quién puede incrustar la página
  9. El patrón correcto: bloquea el sitio, abre una sola ruta de inserción
  10. Nginx (y la trampa de add_header)
  11. Apache
  12. Next.js y Express
  13. Cómo incrustar un widget de terceros de forma segura
  14. sandbox: qué permitir en un widget de calculadora
  15. Por qué no hay solución del lado de quien incrusta
  16. Verifica las cabeceras después de cada cambio
  17. Síntoma → causa → solución
  18. Preguntas frecuentes

Qué significa realmente «ha rechazado la conexión» dentro de un iframe

Añades un <iframe> que apunta a otro sitio, recargas y, en lugar de la página, aparece un recuadro gris con una carita triste; al pasar el cursor por encima se lee La página example.com ha rechazado la conexión. Tu primer instinto es pensar en un problema de red, porque es justo lo que Chrome muestra con un servidor caído. Pero abre DevTools y la consola cuenta otra historia:

text
Refused to display 'https://emicalcs.com/' in a frame because it set 'X-Frame-Options' to 'sameorigin'.

No falló nada en la red. El DNS resolvió, la conexión TCP se abrió, TLS terminó y el servidor devolvió un 200 perfectamente válido. El navegador descargó la página, leyó una cabecera de respuesta y entonces se negó a propósito a dibujarla dentro de tu página. Esa cabecera es X-Frame-Options, y existe para frenar el clickjacking: un ataque en el que una página hostil carga un formulario de login o de pago dentro de un iframe invisible y engaña a los visitantes para que hagan clic en botones que no ven.

Esto importa para diagnosticarlo. Un fallo real de conexión —ERR_CONNECTION_REFUSED al abrir la URL directamente en una pestaña— es un problema de servidor o de firewall, tratado en ERR_CONNECTION_REFUSED. La versión con iframe solo aparece cuando la página va incrustada, y la misma URL funciona bien en su propia pestaña. Si el sitio carga directamente pero no en tu marco, estás ante una política de incrustación, no ante una caída.

Consulta las cabeceras X-Frame-Options y CSP de una página

Cómo funciona X-Frame-Options

X-Frame-Options es una cabecera de respuesta HTTP descrita por primera vez en la RFC 7034, una RFC informativa que el estándar HTML ya sustituye, y la soportan todos los navegadores desde Internet Explorer 8. El servidor la adjunta a una página; el navegador la lee antes de renderizar y decide si la página puede aparecer dentro de un <frame>, <iframe>, <embed> u <object> de otra página.

Admite dos valores que aún importan, más uno que ya no:

ValorQuién puede incrustar la páginaUso típico
DENYNadie, ni siquiera el mismo sitioPáginas de login, paneles de administración, todo lo que lleve sesión
SAMEORIGINSolo páginas del mismo origen exacto (esquema + host + puerto)Valor por defecto sensato para la mayoría de sitios; permite incrustar tus propias páginas
ALLOW-FROM https://a.comObsoleto: lo ignoran todos los navegadores modernosNo lo uses; ver más abajo

La cabecera se aplica por respuesta, no por sitio. Es el detalle que omiten la mayoría de guías y el que hace posible la solución de este artículo: un servidor puede enviar SAMEORIGIN en el 99 % de sus páginas y otra política en la única ruta pensada para incrustarse.

DENY o SAMEORIGIN: ¿cuál enviar?

Envía SAMEORIGIN salvo que tengas un motivo para no hacerlo. Bloquea cualquier marco de terceros y deja a tu sitio libre de incrustar sus propias páginas: widgets de ayuda, vistas previas, paneles que cargan otras páginas internas.

Envía DENY en las páginas donde incrustar no tiene ningún fin legítimo y el daño sería grave: formularios de acceso, páginas de pago, acciones de un clic como «eliminar cuenta». DENY también cubre un caso sutil: que un atacante consiga inyectar un marco en una de tus propias páginas; con SAMEORIGIN, ese marco inyectado seguiría permitido.

Elijas la que elijas, defínela una vez en el servidor web o en el CDN para que cubra todas las respuestas, incluidas las páginas de error y los archivos estáticos. Las cabeceras puestas en el código de la aplicación suelen olvidar la página 404 y la de mantenimiento: ponlas donde pasan todas las respuestas.

ALLOW-FROM está muerto: no lo uses

Artículos antiguos aún recomiendan X-Frame-Options: ALLOW-FROM https://partner.com para permitir un único sitio que incruste. Nunca funcionó en Chrome ni en Safari, y Firefox lo eliminó en la versión 70 (octubre de 2019). Peor aún, un navegador moderno que ve ALLOW-FROM trata toda la cabecera como inválida y la ignora por completo, lo que deja la página incrustable por cualquiera: lo contrario de lo que querías.

Si necesitas permitir sitios concretos, la respuesta es CSP frame-ancestors, que se explica a continuación. X-Frame-Options solo sabe decir «nadie» o «solo yo».

frame-ancestors: la directiva CSP que lo reemplaza

Content Security Policy nivel 2 introdujo frame-ancestors, una directiva que hace todo lo que hace X-Frame-Options y añade lo que este no puede: una lista de permitidos real. Va dentro de la cabecera estándar Content-Security-Policy y acepta una o varias fuentes:

http
# Nobody may frame this (same as X-Frame-Options: DENY)
Content-Security-Policy: frame-ancestors 'none'

# Only my own origin (same as SAMEORIGIN)
Content-Security-Policy: frame-ancestors 'self'

# My origin plus two named partners, one with a subdomain wildcard
Content-Security-Policy: frame-ancestors 'self' https://partner.example https://*.trusted.example

# Anyone may frame this — for a public embed endpoint only
Content-Security-Policy: frame-ancestors *

Conviene conocer tres detalles antes de cambiar:

  • Comprueba todos los ancestros, no solo el padre. Si la página A incrusta la página B y B incrusta la tuya, tanto A como B deben estar permitidas (CSP). El estándar HTML ahora aplica X-Frame-Options también a todos los ancestros (WHATWG), pero los navegadores antiguos solo comprobaban el padre o la página superior.
  • Solo funciona como cabecera HTTP. frame-ancestors se ignora explícitamente cuando la CSP llega mediante una etiqueta <meta http-equiv>: el navegador tiene que conocer la política antes de decidir si renderiza el documento. Con X-Frame-Options pasa lo mismo. Ninguna de las dos se puede definir desde el HTML.
  • Los comodines valen por host, no globalmente. https://*.example.com coincide con cualquier subdominio de un sitio; un * a secas coincide con todos los orígenes de internet. Bien para un widget de calculadora, mal para cualquier cosa con sesión.

Qué pasa cuando están las dos cabeceras

La mayoría de sitios acaban enviando ambas durante una migración, y la precedencia está definida: si frame-ancestors está presente, el navegador lo aplica e ignora X-Frame-Options. La especificación CSP nivel 2 lo dice sin rodeos, y Chrome, Firefox, Safari y Edge la siguen.

Eso hace sencilla la migración segura. Mantén X-Frame-Options: SAMEORIGIN para los pocos clientes antiguos anteriores a CSP 2, añade Content-Security-Policy: frame-ancestors 'self' a su lado y todos los navegadores modernos usarán la regla CSP. Las dos solo dan problemas si les haces decir cosas distintas —X-Frame-Options: DENY junto a frame-ancestors *, por ejemplo—, en cuyo caso los navegadores modernos permiten la incrustación y solo bloquean los muy viejos. Mantenlas coherentes y no hay nada que depurar.

Paso 1: encuentra la cabecera que te bloquea

Antes de cambiar nada, confirma qué política está en juego. La consola ya te dio el nombre de la cabecera; curl te muestra el valor exacto que envía el servidor. Este ejemplo usa el sitio del caso práctico más abajo: sus páginas principales están bloqueadas a propósito:

bash
curl -sI https://emicalcs.com/ | grep -i -E 'x-frame-options|content-security-policy'

# Output:
# x-frame-options: SAMEORIGIN

Te devolverá una de estas tres cosas:

  • x-frame-options: SAMEORIGIN o DENY: la política clásica de incrustación. Solo un cambio en ese servidor puede levantarla.
  • content-security-policy: … frame-ancestors …: una política CSP. Lee la lista de fuentes; si tu origen no está, estás bloqueado. La directiva suele estar enterrada en una cabecera CSP larga junto a script-src y compañía, así que lee el valor entero.
  • Ninguna de las dos cabeceras: el sitio incrustado permite el marco y «ha rechazado la conexión» tiene otra causa: una redirección a una página que sí está bloqueada (a menudo un login; síguela con curl -sIL), o una cabecera Cross-Origin-Embedder-Policy: require-corp en tu propia página, que bloquea cualquier marco que no lo acepte y muestra el mismo recuadro gris.

Paso 2: decide quién puede incrustar la página

Todo lo que sigue depende de una pregunta: ¿controlas el sitio que se incrusta?

Si no, detente aquí. No hay ningún truco del lado del cliente: ni un atributo sandbox, ni otro src, ni una opción del navegador que tus visitantes vayan a tener. El dueño del sitio decidió que su página no se incruste, y el navegador hace cumplir esa decisión en su nombre. Tus opciones son enlazar a la página, preguntar al dueño si ofrece una ruta de inserción o usar un widget oficial si existe. Muchos servicios que bloquean su sitio principal ofrecen uno; el ejemplo de la sección siguiente muestra cómo es.

Si lo controlas, resiste la tentación de quitar la cabecera en todo el sitio. Eso reabre el clickjacking en todas las páginas, incluidas las que tienen sesiones y formularios, para arreglar una inserción en una sola. Decide mejor qué rutas concretas deben poder incrustarse y por quién:

SituaciónCabecera que enviar en esa ruta
Herramienta interna incrustada por tu propio panelframe-ancestors 'self' (más X-Frame-Options: SAMEORIGIN)
Widget incrustado por unos pocos socios conocidosframe-ancestors 'self' https://partner-a.com https://partner-b.com
Widget público incrustable (calculadora, mapa, reproductor)frame-ancestors * solo en la ruta de inserción
Todo lo demásframe-ancestors 'self' más X-Frame-Options: SAMEORIGIN

El patrón correcto: bloquea el sitio, abre una sola ruta de inserción

El ejemplo real más limpio de este patrón que he encontrado es EMICalcs, una colección gratuita de 45 calculadoras financieras indias (cuota de préstamo, SIP, impuesto sobre la renta, GST y más) que otros sitios pueden incrustar. El sitio principal está bloqueado; una ruta dedicada /embed/ está abierta. Compara las cabeceras de ambas:

bash
# The calculator page a visitor normally uses — locked
curl -sI https://emicalcs.com/income-tax-calculator/ | grep -i -E 'x-frame|frame-ancestors'
# x-frame-options: SAMEORIGIN

# The same calculator on its embed path — open to any site
curl -sI https://emicalcs.com/embed/income-tax-calculator/ | grep -i -E 'x-frame|frame-ancestors'
# content-security-policy: frame-ancestors *

Fíjate en lo que la respuesta de inserción no contiene: no hay ninguna cabecera X-Frame-Options en /embed/. Es deliberado. Como la directiva CSP tiene prioridad, dejar SAMEORIGIN sería inofensivo en los navegadores modernos, pero confundiría al próximo ingeniero que lea las cabeceras y seguiría bloqueando a los pocos clientes antiguos que solo entienden X-Frame-Options. La ruta de inserción envía una política y dice una sola cosa.

Todo lo demás del sitio mantiene SAMEORIGIN, así que la página de inicio, los artículos y las páginas completas de las calculadoras siguen protegidos contra el clickjacking. Las versiones incrustables son documentos reducidos, sin navegación y sin nada que secuestrar, y por eso abrirlas a * es un compromiso razonable.

Su guía de inserción vale la pena como plantilla si vas a publicar tu propio widget: una URL estable por calculadora, una altura fija que sirve para la mayoría, loading="lazy" en el fragmento y la promesa de que las URL de inserción retiradas redirigirán en lugar de dar 404, porque cada sitio que te incrusta confía en que tu URL siga funcionando.

Nginx (y la trampa de add_header)

Aquí tienes el mismo patrón en los tres sitios donde la mayoría define cabeceras. En cada caso, una regla global lo bloquea todo y una regla más estrecha la anula en la ruta de inserción. Primero Nginx, porque tiene una trampa:

nginx
# Site-wide default (server block)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;

location /embed/ {
    # WARNING: any add_header here stops ALL add_header inheritance from the
    # server block — re-declare every header you still want on this path.
    add_header Content-Security-Policy "frame-ancestors *" always;
    add_header X-Content-Type-Options "nosniff" always;
}

El comentario no es opcional. add_header no se fusiona entre niveles: en cuanto un bloque location declara cualquier add_header, deja de heredar todos los add_header del bloque server. Hay equipos que pierden así HSTS o nosniff en la ruta de inserción y lo descubren meses después en un escaneo de seguridad. Vuelve a declarar lo que necesites y deja X-Frame-Options fuera del bloque de inserción para que la ruta envíe una política clara. Desde nginx 1.29.3, add_header_inherit merge puede reactivar la herencia (nginx), pero no lo uses en el bloque de inserción: volvería a añadir a esa ruta el X-Frame-Options y el frame-ancestors 'self' de todo el sitio, y con dos cabeceras CSP el navegador aplica ambas, así que el marco sigue bloqueado.

Apache

La directiva Header de Apache se fusiona entre ámbitos, así que puedes definir el valor por defecto una vez y tocar solo las dos cabeceras que cambian en la ruta de inserción:

apache
# Site-wide default (needs mod_headers)
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"

<LocationMatch "^/embed/">
    Header always unset X-Frame-Options
    Header always set Content-Security-Policy "frame-ancestors *"
</LocationMatch>

Next.js y Express

En Next.js, las cabeceras se declaran en next.config.js. Cuando dos reglas coinciden con la misma ruta y definen la misma cabecera, gana la última, pero una cabecera definida solo por la primera sigue aplicándose. Para dejar X-Frame-Options totalmente fuera de la ruta de inserción, exclúyela de la regla global con una anticipación negativa (negative lookahead):

javascript
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        // Every path EXCEPT /embed/... gets the locked defaults
        source: '/((?!embed/).*)',
        headers: [
          { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
          { key: 'Content-Security-Policy', value: "frame-ancestors 'self'" },
        ],
      },
      {
        source: '/embed/:path*',
        headers: [
          { key: 'Content-Security-Policy', value: 'frame-ancestors *' },
        ],
      },
    ]
  },
}

En Express, el popular middleware helmet ya envía por defecto X-Frame-Options: SAMEORIGIN y una CSP que incluye frame-ancestors 'self'. Móntalo de forma global y, en el router de inserción, llama a res.removeHeader('X-Frame-Options') y define Content-Security-Policy como frame-ancestors * antes de renderizar. El orden importa: tu anulación tiene que ejecutarse después de helmet, o helmet la sobrescribirá.

Cómo incrustar un widget de terceros de forma segura

Ahora, el otro lado de la valla: eres el sitio que incrusta, el proveedor tiene una ruta de inserción abierta y quieres que el marco se comporte. Un fragmento de widget bien hecho se ve así; es el que EMICalcs publica en cada página de calculadora:

html
<iframe src="https://emicalcs.com/embed/income-tax-calculator/"
        title="Income Tax Calculator FY 2026-27 — EMICalcs"
        width="100%" height="760"
        style="max-width:680px;border:0;border-radius:16px;overflow:hidden"
        loading="lazy"></iframe>
<p>Free <a href="https://emicalcs.com/income-tax-calculator/" target="_blank" rel="noopener">Income Tax Calculator FY 2026-27</a> by EMICalcs</p>

Cada atributo cumple una función:

  • title: da nombre al marco para los lectores de pantalla. Sin él, la tecnología de asistencia anuncia «marco» y nada más, y las auditorías de accesibilidad suspenden la página.
  • width="100%" con un max-width en style: llena las columnas estrechas en móvil sin desbordar las anchas en escritorio.
  • Un height fijo: la página que incrusta no puede leer la altura del contenido de un marco de otro origen, así que un valor fijo es la única forma de evitar saltos de diseño mientras carga. Defínelo una vez por widget.
  • loading="lazy": aplaza la petición hasta que el marco se acerca a la zona visible, de modo que un widget al final de un artículo largo no cuesta nada en la carga inicial.
  • border:0; border-radius; overflow:hidden: quita el borde hundido por defecto y recorta las esquinas para que el widget parezca nativo en tu página.

La línea de crédito va deliberadamente fuera del iframe. Todo lo que está dentro del marco pertenece al documento incrustado: sus enlaces son los del proveedor, en la URL del proveedor, así que un enlace dentro del marco no forma parte de tu página para tus lectores ni para el rastreador de un buscador. Si quieres dar crédito a la fuente, o el proveedor lo pide, pon un párrafo normal junto al marco en tu propio HTML, como arriba.

sandbox: qué permitir en un widget de calculadora

El atributo sandbox es la protección propia de quien incrusta. Añádelo sin valor y la página incrustada no obtiene nada: ni scripts, ni formularios, ni ventanas emergentes, ni navegar tu ventana, y un origen opaco que desactiva sus propias cookies y su almacenamiento. Después vuelve a añadir solo lo que el widget necesita:

html
<iframe src="https://emicalcs.com/embed/loan-emi-calculator/"
        sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
        title="Loan EMI Calculator — EMICalcs"
        width="100%" height="760" loading="lazy"></iframe>

allow-scripts y allow-forms son el mínimo para cualquier cosa interactiva. allow-same-origin deja que la página incrustada conserve su propio origen para que funcionen su almacenamiento y sus cookies; en una inserción de otro origen es seguro, porque «su origen» sigue siendo el del proveedor, no el tuyo. allow-popups solo hace falta si el widget abre enlaces en una pestaña nueva, como hace el enlace de crédito dentro de la inserción de EMICalcs.

Lo que no debes hacer es combinar allow-scripts con allow-same-origin en un marco que apunta a tu propio origen. El script incrustado puede entonces subir y quitarse su propio atributo sandbox, y no habrás aislado nada.

Por qué no hay solución del lado de quien incrusta

Buscar «refused to connect iframe» devuelve tres soluciones populares. Las tres son malas, y conviene saber exactamente por qué.

  • Extensiones del navegador que quitan X-Frame-Options: solo funcionan en tu propio navegador. Todos los visitantes de tu sitio siguen viendo el recuadro gris, y mientras tanto has desactivado la protección contra el clickjacking en todos los sitios que visitas.
  • Pasar la página por tu propio servidor: descargas el HTML de terceros en el servidor y lo sirves desde tu origen, de modo que la cabecera nunca llega al navegador. Eso rompe los enlaces relativos, las cookies, los inicios de sesión y todo lo que dependa de scripts; te hace responsable de su contenido; y normalmente incumple los términos del proveedor. Los sitios que te detectan bloquean la IP de tu servidor.
  • Servicios de capturas de pantalla o «web a imagen»: obtienes una imagen estática, no una página que funcione. Vale para una miniatura de vista previa, no sirve para una calculadora.

La cabecera es una declaración del dueño del sitio, aplicada por el navegador, sobre lo que puede pasar con su página. Las únicas vías legítimas son las dos ya vistas: que el dueño abra una ruta de inserción o que tú enlaces. Los proveedores que quieren que los incrusten lo ponen fácil: los buenos publican un fragmento, mantienen la URL estable y abren solo la ruta que es seguro abrir.

Verifica las cabeceras después de cada cambio

Las cabeceras de incrustación se definen en un archivo de configuración y luego se olvidan, y un CDN, un proxy inverso o un plugin de seguridad bienintencionado puede añadirlas, quitarlas o duplicarlas en cualquier momento. Tras desplegar, comprueba desde fuera de tu red:

bash
# The locked pages should show SAMEORIGIN and/or frame-ancestors 'self'
curl -sI https://example.com/ | grep -i -E 'x-frame-options|content-security-policy'

# The embed path should show ONLY frame-ancestors, with the sources you intended
curl -sI https://example.com/embed/widget/ | grep -i -E 'x-frame-options|content-security-policy'

# Error pages should carry the header too
curl -sI https://example.com/this-does-not-exist | grep -i -E 'x-frame-options|content-security-policy'

Después pruébalo como lo hace un navegador: pon la URL de inserción en un <iframe> de una página servida desde un origen distinto —una página local file:// no sirve, porque los navegadores la tratan de forma especial— y abre la consola. Una carga limpia significa que no aparece ninguna línea Refused to display ni Refused to frame.

Si el sitio está detrás de Cloudflare u otro CDN, una copia en caché o una regla de borde puede llevar cabeceras distintas a las de tu origen. El Verificador de encabezados HTTP pide la URL desde nuestro servidor, fuera de tu navegador y de tu red, y muestra X-Frame-Options y Content-Security-Policy con un veredicto de incrustación (SAMEORIGIN o DENY, mediante CSP frame-ancestors, o no enviada): una forma rápida de detectar una regla del CDN que haya vuelto a añadir SAMEORIGIN a tu ruta de inserción en silencio.

Síntoma → causa → solución

Usa esto para ir directo a la solución sin releer toda la guía:

Qué vesQué significaSolución
Recuadro gris «ha rechazado la conexión», pero la URL carga en una pestañaX-Frame-Options o frame-ancestors bloqueó el marcoSolo el dueño del sitio puede cambiarlo: usa su ruta de inserción o enlaza
Consola: Refused to display … 'X-Frame-Options' to 'sameorigin'Cabecera clásica; solo se permiten marcos del mismo origenEn el sitio incrustado, abre la ruta de inserción con frame-ancestors * o una lista de socios
Consola: Refused to frame … violates … frame-ancestorsLa lista de permitidos de la CSP no incluye tu origenAñade tu origen a las fuentes frame-ancestors del sitio incrustado
Ayer funcionaba, hoy no, sin cambios de códigoUn CDN o un plugin de seguridad empezó a inyectar SAMEORIGINComprueba con curl; excluye la ruta de inserción en la regla del CDN o del plugin
Tu propio panel no puede incrustar tu propia páginaSubdominio o esquema distinto (www frente al dominio raíz, http frente a https)Usa frame-ancestors 'self' https://example.com https://*.example.com (el comodín no cubre el dominio raíz)
La inserción funciona, pero HSTS o nosniff desaparecieron en esa rutaLa herencia de add_header de Nginx los descartóVuelve a declarar todas las cabeceras dentro del bloque location
ERR_CONNECTION_REFUSED al abrir la URL directamenteUn fallo de red real, no una política de incrustaciónConsulta ERR_CONNECTION_REFUSED y el comprobador de puertos

Si la URL también falla en su propia pestaña, el marco es una pista falsa. Recorre las capas de red en orden —DNS, puerto, TLS, cabeceras— con la guía para probar un endpoint de API público antes de tocar ninguna cabecera de incrustación.

Consulta todas las cabeceras de seguridad que envía una URL

Comprueba X-Frame-Options, Content-Security-Policy, HSTS y el resto de cualquier página, obtenidas desde nuestros servidores, de modo que una extensión del navegador o un proxy de tu propia red no pueda cambiar lo que ves. Gratis, sin registro.

Fuentes

  1. RFC 7034: HTTP Header Field X-Frame-Options, IETF
  2. CSP Level 3: frame-ancestors, W3C
  3. HTML Standard: the X-Frame-Options header, WHATWG
  4. ngx_http_headers_module (add_header, add_header_inherit), nginx

Preguntas frecuentes

¿X-Frame-Options está obsoleto?

Funcionalmente lo ha reemplazado la directiva CSP frame-ancestors, a la que los navegadores dan prioridad cuando están las dos cabeceras. No es inútil, sin embargo: todos los navegadores actuales siguen respetando X-Frame-Options cuando falta frame-ancestors, y protege a los pocos clientes antiguos anteriores a CSP nivel 2. El consejo práctico es enviar ambas y mantenerlas coherentes: SAMEORIGIN junto a frame-ancestors 'self'.

¿Qué significa X-Frame-Options: DENY?

DENY le dice al navegador que la página no debe renderizarse nunca dentro de un elemento frame, iframe, embed u object en ninguna página, incluidas las del propio sitio. Es el ajuste más estricto y el adecuado para formularios de login, páginas de pago y paneles de administración, donde no hay motivo legítimo para incrustar la página y el clickjacking sería grave.

¿Cuál es la diferencia entre DENY y SAMEORIGIN?

DENY bloquea toda incrustación. SAMEORIGIN permite incrustar solo a páginas del mismo origen exacto (mismo esquema, host y puerto) y bloquea a todas las demás. SAMEORIGIN es el valor sensato para todo el sitio porque frena el clickjacking de terceros y deja que tu sitio incruste sus propias páginas; DENY es para el puñado de páginas que nunca deben incrustarse.

¿Cómo esquivo X-Frame-Options para incrustar un sitio?

Desde el lado de quien incrusta, no se puede. La cabecera la define el sitio que intentas incrustar y la hacen cumplir los navegadores de tus visitantes; ningún atributo de iframe, indicador sandbox o truco del cliente lo cambia. Las extensiones que quitan la cabecera solo afectan a tu navegador, y pasar la página por tu servidor la rompe y normalmente incumple los términos del proveedor. Las vías legítimas son usar una ruta de inserción oficial si el proveedor la ofrece, o enlazar a la página.

¿Cómo configuro X-Frame-Options en DENY?

Añádela como cabecera de respuesta HTTP en el servidor web para que cubra todas las páginas, incluidas las de error. En Nginx: add_header X-Frame-Options "DENY" always; en Apache: Header always set X-Frame-Options "DENY". Combínala con Content-Security-Policy: frame-ancestors 'none' para los navegadores modernos. No se puede hacer con una etiqueta meta: solo funciona una cabecera HTTP.

¿Por qué mi iframe dice «ha rechazado la conexión» si el sitio funciona en una pestaña nueva?

Porque la conexión no tiene ningún problema. El sitio devolvió la página con normalidad, pero con una cabecera X-Frame-Options o frame-ancestors que prohíbe renderizarla dentro de otra página. Chrome muestra el mismo recuadro gris «ha rechazado la conexión» que ante un fallo de red real, y por eso confunde; el mensaje de consola «Refused to display … in a frame» es la pista definitiva.

¿Puedo definir X-Frame-Options o frame-ancestors en una etiqueta meta?

No. Ambas deben entregarse como cabeceras de respuesta HTTP. El navegador tiene que conocer la política de incrustación antes de decidir si renderiza el documento, y una etiqueta meta llega demasiado tarde. frame-ancestors se ignora explícitamente en una CSP meta http-equiv, y X-Frame-Options en una etiqueta meta lo ignoran todos los navegadores.

¿Hace frame-ancestors * que mi sitio sea inseguro?

Solo en las respuestas que lo envían. La cabecera se evalúa por documento, así que enviar frame-ancestors * en una ruta /embed/ dedicada deja el resto del sitio plenamente protegido por SAMEORIGIN o frame-ancestors 'self'. El riesgo se limita a lo que un atacante podría ganar incrustando la propia página de inserción, que en un widget sin estado, sin cookie de sesión y sin acciones sensibles es nada.

¿Por qué no funciona X-Frame-Options: ALLOW-FROM?

ALLOW-FROM solo lo implementaron Internet Explorer, el Edge original (versiones 12 a 18) y Firefox, que lo eliminó en la versión 70. Chrome y Safari nunca lo soportaron. Los navegadores modernos tratan una cabecera con ALLOW-FROM como inválida y la ignoran por completo, lo que deja la página incrustable por cualquiera. Usa la directiva CSP frame-ancestors, que admite una lista real de orígenes permitidos.

¿Qué cabecera gana si están X-Frame-Options y frame-ancestors?

frame-ancestors. El estándar HTML indica a los navegadores que ignoren X-Frame-Options siempre que una CSP aplicada contenga frame-ancestors (CSP nivel 2 ya lo recomendaba), y todos los navegadores actuales lo hacen; una política Report-Only no cuenta. Solo los clientes demasiado antiguos para entender CSP recurren a X-Frame-Options, por eso sigue valiendo la pena enviarla, siempre que las dos cabeceras coincidan.

Sobre el autor

Shaik Vahid

Fundador de DNS Robot. Vahid pasó años en el soporte de alojamiento web resolviendo dominios, registros DNS, certificados SSL y la entrega de correo para clientes. Desarrolla las herramientas de DNS Robot y escribe estas guías a partir de los errores que la gente encuentra de verdad.

Verificador de encabezados HTTP

Analiza cabeceras de respuesta HTTP y clasificación de cabeceras de seguridad