iframe не позволяет установить соединение: X-Frame-Options

Ключевой вывод

Сообщение «не позволяет установить соединение» внутри iframe — не сетевая ошибка. Встраиваемый сайт отправил заголовок X-Frame-Options или CSP frame-ancestors, который запрещает браузеру показывать его внутри другой страницы. Исправлять нужно на сервере встраиваемого сайта, а не в теге iframe: оставьте SAMEORIGIN для всего сайта и откройте один путь для встраивания через frame-ancestors *.

Проверьте сейчас

X-Frame-Options: iframe с сообщением «не позволяет установить соединение» и решение — SAMEORIGIN для всего сайта, CSP frame-ancestors * только для пути embed
X-Frame-Options: iframe с сообщением «не позволяет установить соединение» и решение — SAMEORIGIN для всего сайта, CSP frame-ancestors * только для пути embed
Начать чтение
  1. Что на самом деле значит «не позволяет установить соединение» в iframe
  2. Как работает X-Frame-Options
  3. DENY или SAMEORIGIN: что отправлять
  4. ALLOW-FROM мёртв — не используйте его
  5. frame-ancestors: директива CSP, которая его заменяет
  6. Что происходит, если заданы оба заголовка
  7. Шаг 1. Найдите блокирующий заголовок
  8. Шаг 2. Решите, кому разрешить встраивать страницу
  9. Правильный шаблон: закрыть сайт, открыть один путь для встраивания
  10. Nginx (и ловушка add_header)
  11. Apache
  12. Next.js и Express
  13. Безопасное встраивание стороннего виджета
  14. sandbox: что разрешить виджету-калькулятору
  15. Почему на стороне встраивающего исправить нельзя
  16. Проверяйте заголовки после каждого изменения
  17. Симптом → причина → решение
  18. Часто задаваемые вопросы

Что на самом деле значит «не позволяет установить соединение» в iframe

Вы добавляете <iframe> с адресом другого сайта, перезагружаете страницу и вместо содержимого видите серый блок с грустным смайликом. Если навести на него курсор, появится надпись Сайт example.com не позволяет установить соединение. Первая мысль — проблема с сетью, ведь именно так Chrome показывает недоступный сервер. Но откройте DevTools, и консоль расскажет другое:

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

В сети ничего не сломалось. DNS разрешился, TCP-соединение открылось, TLS завершился, сервер вернул совершенно нормальный ответ 200. Браузер скачал страницу, прочитал один заголовок ответа и намеренно отказался отображать её внутри вашей страницы. Этот заголовок — X-Frame-Options, и нужен он для защиты от кликджекинга: атаки, при которой вредоносная страница загружает форму входа или оплаты в невидимый iframe и заставляет посетителя нажимать кнопки, которых он не видит.

Это важно для диагностики. Настоящий сбой соединения — ERR_CONNECTION_REFUSED при открытии URL напрямую во вкладке — это проблема сервера или файрвола, она разобрана в статье ERR_CONNECTION_REFUSED. Вариант с iframe возникает только когда страница встроена, а тот же URL в отдельной вкладке работает. Если сайт открывается напрямую, но не во фрейме, перед вами политика встраивания, а не сбой.

Посмотрите X-Frame-Options и CSP-заголовки страницы

Как работает X-Frame-Options

X-Frame-Options — HTTP-заголовок ответа, впервые описанный в RFC 7034. Это информационный RFC, который теперь заменяет стандарт HTML; заголовок поддерживают все браузеры, начиная с Internet Explorer 8. Сервер добавляет его к странице, а браузер читает его перед отрисовкой и решает, можно ли показывать страницу внутри <frame>, <iframe>, <embed> или <object> на другой странице.

Из значений актуальны два, ещё одно уже нет:

ЗначениеКто может встроить страницуТипичное применение
DENYНикто, даже тот же сайтСтраницы входа, админ-панели, всё, где есть сессия
SAMEORIGINТолько страницы с точно таким же источником (схема + хост + порт)Разумное значение по умолчанию для большинства сайтов; позволяет встраивать собственные страницы
ALLOW-FROM https://a.comУстарело — игнорируется всеми современными браузерамиНе использовать; см. ниже

Заголовок действует на каждый ответ отдельно, а не на сайт целиком. Большинство руководств об этом умалчивает, а именно это делает возможным решение из статьи: сервер может отправлять SAMEORIGIN на 99% страниц и другую политику на одном пути, предназначенном для встраивания.

DENY или SAMEORIGIN: что отправлять

Отправляйте SAMEORIGIN, если нет причин поступить иначе. Он блокирует все сторонние фреймы, а ваш сайт по-прежнему может встраивать собственные страницы: виджеты помощи, предпросмотры, дашборды, подгружающие другие внутренние страницы.

DENY отправляйте на страницах, где встраивание не имеет законной цели, а ущерб от него велик: формы входа, страницы оплаты, действия в один клик вроде «удалить аккаунт». DENY закрывает и тонкий случай, когда злоумышленнику удаётся внедрить фрейм в одну из ваших страниц: при SAMEORIGIN такой внедрённый фрейм всё равно был бы разрешён.

Что бы вы ни выбрали, задавайте заголовок один раз на уровне веб-сервера или CDN, чтобы он попадал в каждый ответ, включая страницы ошибок и статические файлы. Заголовки из кода приложения часто пропускают страницу 404 и страницу техработ, поэтому задавайте их там, где проходят все ответы.

ALLOW-FROM мёртв — не используйте его

Старые статьи до сих пор советуют X-Frame-Options: ALLOW-FROM https://partner.com, чтобы разрешить встраивание одному сайту. В Chrome и Safari это никогда не работало, а Firefox отказался от него в версии 70 (октябрь 2019). Хуже того, современный браузер, увидев ALLOW-FROM, считает весь заголовок недопустимым и полностью игнорирует его, так что страницу может встроить кто угодно — ровно противоположное задуманному.

Если нужно разрешить конкретные сайты, ответ — CSP frame-ancestors, о нём дальше. X-Frame-Options умеет сказать только «никто» или «только я».

frame-ancestors: директива CSP, которая его заменяет

Content Security Policy Level 2 добавила frame-ancestors — директиву, которая умеет всё, что умеет X-Frame-Options, и ещё то, чего он не умеет: настоящий список разрешённых источников. Она записывается в стандартный заголовок Content-Security-Policy и принимает один или несколько источников:

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 *

Три детали стоит знать до перехода:

  • Проверяются все предки, а не только родитель. Если страница A встраивает страницу B, а B встраивает вашу, разрешены должны быть и A, и B (CSP). Стандарт HTML теперь применяет X-Frame-Options тоже ко всем предкам (WHATWG), но старые браузеры проверяли только родителя или верхнюю страницу.
  • Работает только как HTTP-заголовок. Если CSP передан через тег <meta http-equiv>, frame-ancestors намеренно игнорируется: браузеру нужно знать политику до того, как он решит, отрисовывать ли документ вообще. То же верно для X-Frame-Options. Ни тот, ни другой нельзя задать из HTML.
  • Подстановочные знаки действуют на хост, а не глобально. https://*.example.com совпадает с любым поддоменом одного сайта; одиночная * — с любым источником в интернете. Для виджета-калькулятора годится, для всего, где есть сессия, — нет.

Что происходит, если заданы оба заголовка

Во время миграции многие сайты отправляют оба заголовка, и приоритет определён: если frame-ancestors присутствует, браузер применяет его и игнорирует X-Frame-Options. Об этом прямо сказано в спецификации CSP Level 2, и Chrome, Firefox, Safari и Edge ей следуют.

Поэтому безопасный путь миграции прост. Оставьте X-Frame-Options: SAMEORIGIN для немногих древних клиентов, не знающих CSP 2, добавьте рядом Content-Security-Policy: frame-ancestors 'self' — и все современные браузеры будут использовать правило CSP. Проблемы возникают, только если заголовки говорят разное — например, X-Frame-Options: DENY рядом с frame-ancestors *: тогда современные браузеры разрешат встраивание, а заблокируют его только очень старые. Держите заголовки согласованными, и отлаживать будет нечего.

Шаг 1. Найдите блокирующий заголовок

Прежде чем что-то менять, убедитесь, какая политика действует. Имя заголовка уже показала консоль браузера, а curl покажет точное значение, которое отправляет сервер. В примере используется сайт из разбора ниже: его основные страницы намеренно закрыты.

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

# Output:
# x-frame-options: SAMEORIGIN

Вернётся один из трёх вариантов:

  • x-frame-options: SAMEORIGIN или DENY — классическая политика встраивания. Снять её может только изменение на том сервере.
  • content-security-policy: … frame-ancestors … — политика CSP. Прочитайте список источников: если вашего источника в нём нет, вы заблокированы. Директива часто спрятана в длинном CSP-заголовке рядом с script-src и другими, поэтому читайте значение целиком.
  • Ни одного из заголовков: встраиваемый сайт фрейм разрешает, а у «не позволяет установить соединение» другая причина — перенаправление на страницу, которая закрыта (часто страница входа; пройдите по нему через curl -sIL), либо заголовок Cross-Origin-Embedder-Policy: require-corp на вашей собственной странице: он блокирует любой фрейм, не давший согласия, и показывает тот же серый блок.

Шаг 2. Решите, кому разрешить встраивать страницу

Всё дальнейшее зависит от одного вопроса: вы управляете встраиваемым сайтом?

Если нет, остановитесь. Обхода на стороне клиента не существует — ни атрибут sandbox, ни другой src, ни флаг браузера, которого нет у ваших посетителей. Владелец сайта решил, что его страницу встраивать нельзя, и браузер исполняет это решение от его имени. Остаётся дать ссылку на страницу, спросить владельца, есть ли у него эндпоинт для встраивания, или использовать официальный виджет, если он есть. Многие сервисы, закрывающие основной сайт, такой виджет предлагают; пример в следующем разделе показывает, как это выглядит.

Если управляете, не поддавайтесь соблазну убрать заголовок со всего сайта. Это снова откроет кликджекинг на каждой странице, включая те, где есть сессии и формы, ради одного встраивания. Вместо этого определите, какие именно пути предназначены для встраивания и кем:

СитуацияЗаголовок для этого пути
Внутренний инструмент, встроенный в ваш дашбордframe-ancestors 'self' (и X-Frame-Options: SAMEORIGIN)
Виджет, встраиваемый несколькими известными партнёрамиframe-ancestors 'self' https://partner-a.com https://partner-b.com
Публичный виджет для встраивания (калькулятор, карта, плеер)frame-ancestors * только на пути встраивания
Всё остальноеframe-ancestors 'self' и X-Frame-Options: SAMEORIGIN

Правильный шаблон: закрыть сайт, открыть один путь для встраивания

Самый чистый реальный пример этого шаблона, который мне встречался, — EMICalcs, бесплатный набор из 45 индийских финансовых калькуляторов (EMI по кредиту, SIP, подоходный налог, GST и другие), которые другим сайтам предлагается встраивать. Основной сайт закрыт, а отдельный путь /embed/ открыт. Сравните заголовки на обоих:

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 *

Обратите внимание на то, чего в ответе для встраивания нет: на /embed/ вообще нет заголовка X-Frame-Options. Это сделано намеренно. Поскольку директива CSP приоритетнее, оставленный SAMEORIGIN в современных браузерах был бы безвреден, но он сбил бы с толку следующего инженера, читающего заголовки, и всё равно блокировал бы немногие старые клиенты, понимающие только X-Frame-Options. Путь для встраивания отправляет одну политику, и она говорит ровно одно.

Всё остальное на сайте сохраняет SAMEORIGIN, поэтому главная страница, статьи и полные страницы калькуляторов защищены от кликджекинга. Версии для встраивания — облегчённые документы без навигации и без того, что стоило бы перехватывать, поэтому открыть их для * — разумный компромисс.

Их руководство по встраиванию стоит прочитать как шаблон, если вы публикуете собственный виджет: один стабильный URL на калькулятор, фиксированная высота, подходящая большинству, loading="lazy" в сниппете и обещание, что снятые с публикации URL будут перенаправлять, а не отдавать 404, — ведь каждый сайт, который вас встраивает, доверяет тому, что ваш URL продолжит работать.

Nginx (и ловушка add_header)

Тот же шаблон в трёх местах, где чаще всего задают заголовки. В каждом случае общее правило закрывает всё, а более узкое переопределяет его для пути встраивания. Сначала Nginx, потому что в нём есть ловушка:

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

Комментарий пропускать нельзя. add_header не объединяется между уровнями: как только блок location объявляет любой add_header, он перестаёт наследовать все add_header из блока server. Так команды регулярно теряют HSTS или nosniff на пути встраивания и узнают об этом лишь через месяцы, по результатам сканирования безопасности. Объявите заново всё нужное, а X-Frame-Options в блок встраивания не добавляйте, чтобы путь отправлял одну ясную политику. Начиная с nginx 1.29.3 add_header_inherit merge может вернуть наследование (nginx), но в блоке встраивания его применять не стоит: он вернёт на этот путь общие X-Frame-Options и frame-ancestors 'self', а при двух заголовках CSP браузер применяет оба, так что фрейм останется заблокированным.

Apache

Директива Header в Apache объединяется между областями, поэтому значение по умолчанию можно задать один раз и менять на пути встраивания только два заголовка:

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 и Express

В Next.js заголовки объявляются в next.config.js. Если два правила совпадают с одним путём и задают один и тот же заголовок, побеждает более позднее, но заголовок, заданный только ранним правилом, всё равно применяется. Чтобы X-Frame-Options не попадал на путь встраивания вовсе, исключите его из общего правила отрицательным просмотром вперёд:

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 *' },
        ],
      },
    ]
  },
}

В Express популярный middleware helmet по умолчанию уже отправляет X-Frame-Options: SAMEORIGIN и CSP с frame-ancestors 'self'. Подключите его глобально, а в роутере встраивания вызовите res.removeHeader('X-Frame-Options') и задайте Content-Security-Policy как frame-ancestors * перед отрисовкой. Порядок важен: ваше переопределение должно выполняться после helmet, иначе helmet его перезапишет.

Безопасное встраивание стороннего виджета

Теперь с другой стороны: вы — сайт, который встраивает, у провайдера есть открытый путь для встраивания, и вы хотите, чтобы фрейм работал как надо. Хорошо оформленный сниппет виджета выглядит так; именно его публикует EMICalcs на каждой странице калькулятора:

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>

Каждый атрибут выполняет свою работу:

  • title — называет фрейм для программ чтения с экрана. Без него вспомогательные технологии произносят только «фрейм», а аудит доступности не пропускает страницу.
  • width="100%" с max-width в style — заполняет узкие колонки на мобильных и не вылезает за широкие на десктопе.
  • Фиксированная height — встраивающая страница не может прочитать высоту содержимого кросс-доменного фрейма, поэтому жёстко заданное значение — единственный способ избежать сдвига макета при загрузке. Задайте его один раз для виджета.
  • loading="lazy" — откладывает запрос, пока фрейм не окажется рядом с областью просмотра, поэтому виджет внизу длинной статьи ничего не стоит при первой загрузке.
  • border:0; border-radius; overflow:hidden — убирает рамку по умолчанию и скругляет углы, чтобы виджет выглядел родным для вашей страницы.

Строка с указанием источника намеренно находится вне iframe. Всё внутри фрейма принадлежит встроенному документу: его ссылки — это ссылки провайдера на URL провайдера, поэтому ссылка внутри фрейма не считается частью вашей страницы ни для читателей, ни для поискового робота. Если вы хотите указать источник или об этом просит провайдер, поставьте обычный абзац рядом с фреймом в собственном HTML, как выше.

sandbox: что разрешить виджету-калькулятору

Атрибут sandbox — собственная защита встраивающей стороны. Добавьте его без значения, и встроенная страница не получит ничего: ни скриптов, ни форм, ни всплывающих окон, ни навигации вашего окна, а её источник станет непрозрачным, что отключит её cookie и хранилище. Затем верните только то, что нужно виджету:

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 и allow-forms — минимум для всего интерактивного. allow-same-origin позволяет встроенной странице сохранить собственный источник, чтобы работали её хранилище и cookie; для кросс-доменного встраивания это безопасно, потому что «её источник» остаётся источником провайдера, а не вашим. allow-popups нужен, только если виджет открывает ссылки в новой вкладке, как делает ссылка на источник внутри встраиваемого EMICalcs.

Чего делать не стоит — сочетать allow-scripts с allow-same-origin во фрейме, который указывает на ваш собственный источник. Скрипт внутри сможет дотянуться наверх и удалить собственный атрибут sandbox, и вы ничего не изолируете.

Почему на стороне встраивающего исправить нельзя

Поиск по запросу «iframe не позволяет установить соединение» выдаёт три популярных обходных пути. Все они плохи, и полезно понимать, почему именно.

  • Расширения браузера, убирающие X-Frame-Options — работают только в вашем браузере. Каждый посетитель вашего сайта по-прежнему видит серый блок, а вы тем временем отключили защиту от кликджекинга на всех сайтах, которые посещаете.
  • Проксирование страницы через ваш сервер — вы получаете чужой HTML на сервере и отдаёте со своего источника, так что заголовок до браузера не доходит. Это ломает относительные ссылки, cookie, авторизацию и всё, что работает на скриптах; вы начинаете отвечать за чужой контент; и обычно это нарушает условия провайдера. Сайты, которые вас поймали, блокируют IP вашего сервера.
  • Сервисы скриншотов («web-to-image») — вы получаете статичную картинку, а не работающую страницу. Для миниатюры предпросмотра сойдёт, для калькулятора бесполезно.

Заголовок — это заявление владельца сайта о том, что можно делать с его страницей, а браузер лишь следит за исполнением. Законных путей только два, о них уже сказано: владелец открывает путь для встраивания, либо вы даёте ссылку. Провайдеры, которые хотят, чтобы их встраивали, делают это просто: хорошие публикуют сниппет, не меняют URL и открывают только тот путь, который безопасно открывать.

Проверяйте заголовки после каждого изменения

Заголовки встраивания задают в конфигурационном файле и забывают, а CDN, обратный прокси или услужливый плагин безопасности могут в любой момент добавить, убрать или продублировать их. После развёртывания проверяйте снаружи вашей сети:

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'

Затем проверьте так, как это делает браузер: поместите URL для встраивания в <iframe> на странице, отдаваемой с другого источника (локальная страница file:// не подойдёт: браузеры обращаются с ней особо), и откройте консоль. Чистая загрузка означает отсутствие строк Refused to display и Refused to frame.

Если сайт стоит за Cloudflare или другим CDN, кэшированная копия или правило на периферии могут нести другие заголовки, чем ваш origin. Проверка HTTP-заголовков запрашивает URL с нашего сервера, вне вашего браузера и сети, и показывает X-Frame-Options и Content-Security-Policy с вердиктом по встраиванию (SAMEORIGIN или DENY, через CSP frame-ancestors либо не отправлен): так быстро находится правило CDN, которое незаметно вернуло SAMEORIGIN на ваш путь встраивания.

Симптом → причина → решение

Используйте таблицу, чтобы сразу перейти к решению, не перечитывая руководство:

Что вы видитеЧто это значитРешение
Серый блок «не позволяет установить соединение», но URL во вкладке открываетсяX-Frame-Options или frame-ancestors заблокировал фреймИзменить может только владелец сайта: используйте его путь для встраивания или дайте ссылку
Консоль: Refused to display … 'X-Frame-Options' to 'sameorigin'Классический заголовок; разрешены только фреймы того же источникаНа встраиваемом сайте откройте путь встраивания через frame-ancestors * или список партнёров
Консоль: Refused to frame … violates … frame-ancestorsВ списке CSP нет вашего источникаДобавьте свой источник в frame-ancestors встраиваемого сайта
Вчера работало, сегодня нет, код не менялсяCDN или плагин безопасности начал добавлять SAMEORIGINПроверьте через curl; исключите путь встраивания из правила CDN или плагина
Ваш дашборд не может встроить вашу же страницуНесовпадение поддомена или схемы (www и корневой домен, http и https)Используйте frame-ancestors 'self' https://example.com https://*.example.com (подстановка не покрывает голый домен)
Встраивание работает, но HSTS или nosniff на этом пути пропалиНаследование add_header в Nginx их сбросилоОбъявите заново все заголовки внутри блока location
ERR_CONNECTION_REFUSED при прямом открытии URLНастоящий сбой сети, а не политика встраиванияСм. ERR_CONNECTION_REFUSED и проверку портов

Если URL не открывается и в отдельной вкладке, фрейм — ложный след. Пройдите уровни сети по порядку — DNS, порт, TLS, заголовки — по руководству по проверке публичного эндпоинта, прежде чем трогать заголовки встраивания.

Все заголовки безопасности, которые отправляет URL

Проверьте X-Frame-Options, Content-Security-Policy, HSTS и другие заголовки любой страницы. Запрос идёт с наших серверов, поэтому расширение браузера или прокси в вашей сети не исказят результат. Бесплатно, без регистрации.

Источники

  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

Часто задаваемые вопросы

X-Frame-Options устарел?

Функционально его заменила директива CSP frame-ancestors, которой браузеры отдают приоритет, если присутствуют оба заголовка. Но бесполезным он не стал: все актуальные браузеры по-прежнему учитывают X-Frame-Options, когда frame-ancestors нет, и он защищает немногие старые клиенты, не знающие CSP Level 2. На практике отправляйте оба заголовка и следите, чтобы они не противоречили друг другу: SAMEORIGIN вместе с frame-ancestors 'self'.

Что означает X-Frame-Options: DENY?

DENY сообщает браузеру, что страницу нельзя отображать внутри элементов frame, iframe, embed и object ни на какой странице, включая страницы того же сайта. Это самая строгая настройка, и она подходит для форм входа, страниц оплаты и админ-панелей, где нет законной причины их встраивать, а кликджекинг был бы серьёзным.

Чем DENY отличается от SAMEORIGIN?

DENY блокирует любое встраивание. SAMEORIGIN разрешает встраивание только страницам ровно того же источника (та же схема, хост и порт) и блокирует остальных. SAMEORIGIN — разумное значение для всего сайта: он останавливает сторонний кликджекинг, но позволяет вашему сайту встраивать собственные страницы; DENY нужен для немногих страниц, которые не должны встраиваться вообще.

Как обойти X-Frame-Options, чтобы встроить сайт?

Со стороны встраивающего — никак. Заголовок задаёт сайт, который вы хотите встроить, а исполняют его браузеры ваших посетителей; ни атрибут iframe, ни флаг sandbox, ни приём на стороне клиента этого не меняют. Расширения, убирающие заголовок, действуют только в вашем браузере, а проксирование страницы через ваш сервер всё ломает и обычно нарушает условия провайдера. Законные пути — воспользоваться официальным эндпоинтом для встраивания, если провайдер его предлагает, либо дать ссылку на страницу.

Как установить X-Frame-Options в DENY?

Добавьте его как HTTP-заголовок ответа на уровне веб-сервера, чтобы он покрывал каждую страницу, включая страницы ошибок. В Nginx: add_header X-Frame-Options "DENY" always; в Apache: Header always set X-Frame-Options "DENY". Для современных браузеров добавьте Content-Security-Policy: frame-ancestors 'none'. Через мета-тег его не задать — работает только HTTP-заголовок.

Почему мой iframe пишет «не позволяет установить соединение», если сайт открывается в новой вкладке?

Потому что с соединением всё в порядке. Сайт вернул страницу как обычно, но с заголовком X-Frame-Options или frame-ancestors, запрещающим отображать её внутри другой страницы. Chrome показывает в этом случае тот же серый блок, что и при настоящем сбое сети, из-за чего и возникает путаница; выдаёт причину сообщение в консоли «Refused to display … in a frame».

Можно ли задать X-Frame-Options или frame-ancestors в мета-теге?

Нет. Оба должны приходить как HTTP-заголовки ответа. Браузеру нужно знать политику встраивания до того, как он решит, отрисовывать ли документ вообще, а мета-тег приходит слишком поздно. frame-ancestors в CSP через meta http-equiv намеренно игнорируется, а X-Frame-Options в мета-теге игнорируют все браузеры.

Делает ли frame-ancestors * мой сайт небезопасным?

Только для ответов, которые его отправляют. Заголовок вычисляется для каждого документа отдельно, поэтому frame-ancestors * на отдельном пути /embed/ оставляет остальной сайт под защитой SAMEORIGIN или frame-ancestors 'self'. Риск ограничен тем, что злоумышленник мог бы получить, встроив саму страницу для встраивания; для виджета без состояния, без cookie сессии и без важных действий это ничего.

Почему X-Frame-Options: ALLOW-FROM не работает?

ALLOW-FROM реализовали только Internet Explorer, исходный Edge (версии 12–18) и Firefox, который убрал его в версии 70. Chrome и Safari его никогда не поддерживали. Современные браузеры считают заголовок с ALLOW-FROM недопустимым и полностью игнорируют, из-за чего страницу может встроить кто угодно. Используйте директиву CSP frame-ancestors, которая поддерживает полноценный список разрешённых источников.

Какой заголовок победит, если заданы и X-Frame-Options, и frame-ancestors?

frame-ancestors. Стандарт HTML велит браузерам игнорировать X-Frame-Options, когда действующий CSP содержит frame-ancestors (CSP Level 2 уже рекомендовал это), и все актуальные браузеры так и делают; политика Report-Only не считается. К X-Frame-Options возвращаются только клиенты, слишком старые для CSP, поэтому его всё ещё стоит отправлять — пока два заголовка согласуются.

Об авторе

Shaik Vahid

Основатель DNS Robot. Вахид много лет работал в поддержке веб-хостинга: исправлял клиентам домены, DNS-записи, SSL-сертификаты и доставку почты. Он создаёт инструменты DNS Robot и пишет эти руководства по ошибкам, с которыми люди сталкиваются на самом деле.

Проверка HTTP-заголовков

Анализ HTTP заголовков ответа и оценка заголовков безопасности