Многие считают, что iframe это что‑то древнее и небезопасное, лучше не марать об это руки и не использовать. У него сложилась довольно грязная репутация. Но, на самом деле, есть ситуации, где он просто незаменим. Побуду неким адвокатом iframe и расскажу, чем он хорош.

Меня зовут Андрей Кузнецов, я занимаюсь версткой с 2005 года, был flash‑ром до 2012 года, сейчас работаю в компании «Рунет Бизнес Системы» frontend‑лидом. Мы занимаемся интернет‑эквайрингом, всевозможными оплатами в Интернете и всеми сопутствующими процессами. Подробно расскажу, что умеет iframe на данный момент времени, о его развитии. Из этих знаний уже можно что‑то лепить, конструировать и решать те самые задачи, которые помогают развиваться бизнесу.

От frame к iframe

Iframe — очень древний тэг, он появился практически на заре самого Интернета, но началось всё не с него, а с тега frame. Его поддержку реализовал первый коммерческий браузер Netscape (именно от него потом появилась Mozilla и остальные) в 1995 году. Спустя пару лет появился сам iframe, но он не набирал популярности. Потому что frame отлично справлялся со своими задачами и использовался почти как микрофронтенд. Например, люди брали меню и туда выгружали отдельную страницу, в которой находилось только меню. А в другом контейнере была страница с самим содержимым.

Я тот самый человек, который это делал. Даже откопал свой первый сайт, где левый свиток — это отдельная страница (отдельный html документ). С помощью атрибута target у ссылки можно было указывать в каком контейнере открывается содержимое.

Это было удобно. Например, при слабом интернете на диалапе вы один раз загрузили меню, оно закешировалась и всё. Дальше идёт только подгрузка содержимого контента, не нужно каждый раз перезагружать страницу. Single page Application ещё в помине не было. Но были проблемы с навигацией по самому сайту. Плюс поисковые роботы очень плохо это всё индексировали. Поэтому начала набирать обороты популярность iframe. Его добавили в спецификацию html-5, а frame похоронили, и теперь он забыт.

Что такое iframe?

Iframe — это контейнер, в который можно загружать любой другой независимый html-документ. На схеме ниже, это голубой прямоугольник.

Наглядный пример, что такое <iframe>
Наглядный пример, что такое <iframe>

Например, загрузить любой другой сайт прямо на страницу. Раньше это было практически невозможно. На этапе становления, у iframe было много проблем с безопасностью. Можно было даже залезть в сам дочерний документ и получить оттуда конфиденциальную информацию. В то время как другой разработчик не знал о том, что iframe кто‑то открыл и читает. С тех пор он получил очень плохую репутацию. Время шло, а вместе с ним, браузеры дорабатывались и закрывали дырки в безопасности iframe.

Сейчас он полностью изолирован от родительского документа. До такой степени, что если в дочернем документе вы пошли в навигацию, меняете url, то родительский документ в source видит тот старый url на момент инициализации. То есть он не знает, что там происходит, даже если мы ушли в какие‑то другие сайты, перешли и так далее. Правда, часто бывает что в общении с безопасниками приходиться им это доказывать, так как у них данные 10-летней давности о возможностях iframe.

Но всё равно из‑за особенностей работы iframe может доставлять неудобства пользователю. Например, вызвать какие‑то ненужные алерты, выполнить redirect, может вылезти куча лишней рекламы и так далее. Поэтому для ограничений появился атрибут sandbox.

Sandbox

<iframe src="..." sandbox="значение"/>

Пустое значение

Включить все ограничения

allow-forms

Разрешить отправку данных форм

allow-pointer-lock

Разрешить использование Pointer Lock API (захват движения мышью)

allow-popups

Разрешить всплывающие окна
(window.open().target="_blank" и др.)

...

Не буду подробно об этом рассказывать. Скажу, что на практике у нас ни разу не использовался атрибут sandbox. Потому что он применяется чаще в учебных целях, либо если вы загружаете какой‑то контент, которому не до конца доверяете. Хотя есть смысл задуматься, стоит ли вообще такое загружать.

Хакерские атаки, устроенные с помощью iframe

Clickjacking

Выглядит она следующим образом. Например, когда вы заходите на какой‑то сайт, вылезает огромная реклама или суперпредложение о том, что вы выиграли в лотерею. Конечно же, там есть кнопочка «Получить выигрыш». Доверчивый пользователь, не задумываясь, на нее тыкает. А дальше может что‑то произойти, о чем пользователь даже не догадывается.

Принцип работы Clickjacking
Принцип работы Clickjacking

Как это работает? Поверх этой кнопки натянут iframe верхним прозрачным слоем. Человек его не видит, а видит только то, что находится под ним. Это кнопка «Забрать выигрыш». Нажимая на неё, он на самом деле выполняет действия на каком‑то другом сайте. Например, подписывается в Твиттере на Илона Маска или ставит кому‑то лайк. В свое время так были взломаны Твиттер, Фейсбук и множество других социальных сетей.

Защита X-Frame Options

Возможные значения заголовка X-Frame-Options

DENY

SAMEORIGIN

ALLOW-FROM domain

Разработчики браузера думали, что делать с clickjacking и придумали отдельный заголовок — X‑Frame Options, который устанавливается со стороны бэкенда. Например, веб‑сервер, с тем же nginx, apache, который говорит браузеру о том, чтобы ваш сайт нельзя было открывать в iframe. Здесь мы уже пытаемся защититься со стороны владельца того сайта, чтобы нас не открыли в iframe. Можно разрешить только собственному домену, где расположено содержимое, либо разрешить внешнему домену.

Когда злоумышленник попытается открыть ваш сайт в iframe, появляется такая вещь:

Наверно, многие встречали это окно. Но у этого заголовка есть проблема — можно указывать только один разрешенный домен.

Представим ситуацию, когда вы сделали какой-то сервис. У вас классная формочка, вы хотите, чтобы её открывали, но только ваши друзья или партнёры. К сожалению, с помощью этого заголовка это сделать нельзя. На помощь приходит достаточно молодая технология — Content Security Policy.

Защита Content Security Policy

На основе него сейчас много чего строится. Здесь можно через пробел указывать множество доверенных доменов, которые смогут вас открывать.

Также можно защититься от того, чтобы ваш сайт открыли в iframe самим java-script’ом изнутри. Вот короткий пример:

Защита. Закроемся div

<style>
  #protector {
    height: 100%; width: 100%; position: absolute;
    left: 0; top: 0; z-index: 99999999;
  }
</style>

<div id="protector">
  <a href="/" target="_blank">Перейти к сайту</a>
</div>

<script>
  if (top.document.domain = = document.domain) {
    protector.remove();
  }
</script>

Можно несколькими способами определить, что вы открыты в iframe. Как вариант, просто растянуть div на всю страницу и сказать: «не делай так». Рабочее решение. Раньше именно так это делалось.

Browser in Browser

Еще появилась достаточно молодая хакерская атака, использующая технологию iframe. Она называется Browser in The Browser (BITB).

В современном мире возможности CSS и Java скрипта настолько богаты, что можно полностью повторить в 1.1 пиксель окна авторизации SSО, которыми мы привыкли пользоваться. Гугл, Гитхаб, Майкрософт, ВКонтакте, Фейсбук — всё что угодно. Выглядит это следующим образом:

Browser In The Browser (BITB)
Browser In The Browser (BITB)

Вы заходите на какой‑нибудь магазин, где очень вкусная цена у айфона. Прямо всё классно. Надо авторизоваться, чтобы оплатить. Вы нажимаете кнопку авторизации с помощью ВКонтакте. И на этот момент времени это был фишинговый сайт. Вылезает точно такая формочка «Авторизуйся во ВКонтакте». Кто‑то подумает, что просто сессия слетела и введёт логин и пароль. И тем самым прямо в открытую, человек вводит логин и пароль злоумышленнику и передаёт эти данные.

Есть хороший совет, как понять, что вас не обманывают. Так как это просто некое содержимое внутри окна, это div, содержащий iframe. Через iframe злоумышленнику намного проще понять, какую авторизацию вы прошли, чтобы подгрузить то самое окно. Можно попробовать утащить это окно за пределы браузера. Стандартное pop up окно без проблем это сделает. А эту штуку утащить не получится. Она находится в DOM‑дереве.

Можно ли получить доступ к iframe при разных доменах?

На текущий момент времени iframe очень хорошо защищён, и родительский сайт ничего не знает, что происходит внутри ребенка.

И наоборот, дочерний документ ничего не знает, что происходит в родительском. Они оба работают независимо. Так можно ли получить доступ от одного к другому?

Стандартная ситуация в нашей работе, какая-нибудь форма оплаты, куда нас встроили, и родительский документ. Партнёр постоянно хочет знать статус оплаты, прошла она или нет, чтобы, например, показать взрыв конфетти, если всё прошло успешно. 

Это можно сделать, если договориться.

Для этого появилась технология Postmessage. Это отдельное сообщение, которое можно слать между iframe.

otherWindow.postMessage (message, targetOrigin) ;

Где:

otherWindow

message

targetOrigin

Если мы не хотим проверять, то в targetOrigin можно указать *.

На самом деле, эта технология работает даже между вкладками браузера. Например, если в одной вы слушаете музыку и начинаете слушать в другой на этом же домене, можно закрыть одну и открыть другую.

Выглядит это следующим образом. Есть просто сообщение message. И важное замечание — есть targetOrigin. Это тот самый источник, который должен получить ваше сообщение. Можно поставить звёздочку, и все, кто слушает это событие, услышат вас. Этого делать не рекомендуется, мало ли какую конфиденциальную информацию вы будете передавать. Поэтому всегда лучше targetOrigin ограничивать каким-то конкретным доменом.

Пример. Закинем информацию в iframe

<iframe src="http://example.com" name="example">

<script>
  let win = window.frames.example;
  win.postMessage("message", "http://example.com") ;
</script>

Мы передаем message на example.com. И в самом iframe мы это ловим с помощью подписывания на событие message.

Пример. Ловим информацию в iframe

window.addEventListener("message", function(event) {
  if (event.origin != "http://frontendconf.ru") {
    // что-то пришло с неизвестного домена. Проигнорируем это
    return;
  }

  alert( "received:" + event.data );
  // Можно отправить ответ через event.source.postMessage(...)
});

Кстати, здесь тоже есть event origin, чтобы вы поняли, тот ли человек это передаёт, тот ли домен нам это делает, чтобы правильно на это среагировать. Потому что могут быть ситуации, когда, например, Яндекс Метрика начнёт вам слать сообщения. Вы начнёте на них реагировать, и они даже могут оказаться по составу и содержимому такого же формата, которого вы ожидали.

Сила iframe

Итак, мы поняли, что умеет iframe в современном мире. Зачем нам это нужно? Представим ситуацию, когда у вас какой‑то классный сервис, у которого есть важная информация, и вы хотите ей делиться. Но не так в открытую, а чтобы это было в определенном дизайне. К примеру, поиск авиабилетов, который блогеры могут вставить себе на сайт. Это должно выглядеть красиво именно под стиль блогера, но вся формочка и логика должны быть ваши. Ещё важно, чтобы блогеру несложно было это сделать.

Понятное дело, это можно реализовать, напрямую сходить в ваше API, которое обладает этой информацией. Но здесь тоже есть подводные камни. Встречается такая проблема, как CORS.

Виджет для партнёра
Виджет для партнёра

CORS

Самый популярный случай, когда вы в dev‑режиме что‑то разрабатываете на localhost и напрямую лезете к себе в бэк, а браузер не разрешает.

Почему браузер по умолчанию запрещает? Это слишком небезопасно. Потому что, если бы это работало, вы бы просто пришли на какой‑то неизвестный сайт, а злоумышленник послал бы запрос на ваш банковский счёт о списании денег. У вас куки есть, сессия передалась, и мы взяли и списали деньги. Чтобы этого не происходило, появился CORS.

На самом деле, CORS можно обойти. Настроить заголовки на backend, чтобы они поддерживались. И даже в этом случае CORS работает нетривиально. Сначала идёт вспомогательный запрос OPTIONS c дополнительной информацией. И только затем появляется полезный запрос с полезной нагрузкой POST.

Например, мы сделали форму поиска авиабилетов и хотим узнать этого пользователя в следующий раз, когда он авторизуется. Backend передает какие‑то куки. Всё вроде бы ничего, всё работает за исключением «классного» браузера Safari. Дело в том, что он не работает с куками с третьей стороны. Он их просто выплёвывает.

Сейчас Safari не принимает
Сейчас Safari по умолчанию не принимает куки третьей стороны

Это достаточно современная история. Заключается она в том, что в настройках по умолчанию в Safari стоит предотвращать перекрестное отслеживание. 

Плюс бывает история, когда у вас эта конфиденциальная информация очень важная, и вам важно, чтобы партнер не имел прямого доступа к ней. Например, как ситуация у нас — это PCI DSS.

PCI DSS

Когда вы платите в Интернете, этот значок всегда висит снизу на платежных страницах. Он обязан там висеть. Он говорит о том, что это доверенный источник, имеющий право получать ваши карточные данные. Если магазин захотел начать принимать данные карт, он обязан проводить мероприятия и сертифицироваться по PCI DSS.

Там есть много уровней “доверия”. Доходит до того, что если вы храните карточные данные, у вас должен быть регулярный аудит data-центров, нужно постоянно заполнять огромные формы. Много таких рутинных вещей, на которые обычные магазины не идут. Это большой риск и много геморроя.

Рассмотрим ситуацию, когда мы хотим сделать простой виджет, чтобы продавец поставил себе кнопочку Apple Pay на сайт и начал принимать оплату. Главное, чтобы это было максимально просто с его стороны, потому что мы не знаем, какой уровень разработчиков у нашего партнёра. 

Есть сайт, например, Shop.com. Мы говорим в документации, ставь себе на страницу наш скрипт и подключи его. Он его подключает, и при инициализации этот скрипт рендерит в DOM-дереве невидимый iframe. На самом деле, этот iframe очень маленький — однопиксельный. Он находится за пределами видимой зоны. Iframe по умолчанию открывает нашу страничку, которая тоже находится на нашей стороне в service.com. Назовем её child.html. В этой странице находится JavaScript — lib-child.js, для примера.

Что нам это даёт? У этой страницы нет никаких ограничений на кросс-доменные запросы. Она находится на своём домене, и может беспрепятственно делать запросы в API. С помощью PostMessage можно всё это связать, чтобы iframe помогал нам делать туннель к нашему API.

Далее мы рендерим кнопочку, например, Apple Pay. Человек на нее нажимает, мы ловим это событие и передаем уже в PostMessage информацию о запросе в iframe. Iframe говорит этому lib-child сделать запрос в API. Он  выполняет это как доверенный нам скрипт, backend отвечает и обратно передаёт на страницу партнёра (также через PostMessage). В Safari мы можем  уже вызвать сессию Apple Pay, чтобы он запросил какую-то биометрию у человека на оплату.

Это и есть основной подход, который сейчас используется iframe. Схема у нас выглядит намного сложнее, но общий подход такой. В итоге у нас не теряются куки, и нет кросс-доменных запросов. При этом владелец сайта — наш партнёр, не имеет доступа к конфиденциальной информации, к карточным данным и так далее. За него всё само происходит, но у него есть возможность вставить эту кнопку именно в то место, где он захочет. Причём не так, чтобы оно вылезало на всю страницу, iframe, pop up и так далее, а это выглядело максимально нативно и родственно для этого сайта.

Сейчас так работает Яндекс Метрика и Google analytics. Они все данные шлют через iframe. Кстати, у нас это была одна из проблем, что надо было защищаться постоянно от их PostMessage и фильтровать их. 

Форма карточной оплаты

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

Опять же можно её вставить полностью как iframe, но продавцы все разные. Например, кто-то хочет, чтобы подписи к полям были жирные. Другому нужно, чтобы это было четыре поля в card number. Или, например, сетку другую. В разных странах привычки ввода карточных данных совершенно разные. Тут, всё в настройки не вынесешь. А если вынести, то потом придётся проклинать себя за то, что это сделал. Потому что поддерживать это будет невозможно.

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

Но есть одна проблема. Например, нам надо сделать активной кнопку «Pay», когда у нас всё корректно заполнено. И тут получается, что очень много событий каждый iframe передает друг другу о своем статусе, состоянии, что у него там происходит. На это становится сложно реагировать.

Плюс это всё асинхронно происходит. Это выглядит, как огромный пинг-понг. Очень быстрый и скоростной.

Или давайте представим ситуацию, когда вам пишут письма с одного конкретного адреса пять разных человек, но у них одинаковые инициалы. И вы не знаете, кто вам это написал, какой-то И.А.  Вам надо грамотно, четко и последовательно ответить каждому. Так, чтобы ваше письмо пришло именно нужному И.А.

Как решать эту задачу? Нам на ум пришло решение, что можно попробовать каждому отдельному человеку дать уникальный псевдоним и вывести это в отдельный канал.

Первую проблему решили. Но он настолько быстро нам спамит сообщениями, что нам нельзя отвлекаться. И именно на письмо номер Х нужно ответить сообщением номер Х. Поэтому можно их пронумеровать. Мы начали отдельно нумеровать эти сообщения. Когда у нас летит сообщение 1023, мы отвечаем письмом 1023 уже со своим ответом. Выглядит это примерно так, описание этих функций:

type ReqMessageObject = {
  type: 'req';
  funcName: string;
  params: any;
  id: string;
};

type ReqMessageObject = {
  type: 'res';
  result: any;
  error: any;
  id: string;
};

Есть request и response. Это такая история, что нам надо что-то отправить на backend или куда-то, посчитать и вернуть. Поэтому там есть функция, параметры и ID-шник, который сам выставляется автоматически. Есть готовые библиотеки, но там проблемы с этим оставлением ID-шников, когда очень много полей. Поэтому вот пример нашего разработчика небольшой библиотеки. Здесь 100 строк кода, где как раз это всё реализуется, и можно очень легко общаться.

Где iframe незаменим

Бывают ситуации, когда iframe просто незаменим. Например, сейчас практически каждая крупная компания делает свой собственный способ оплаты, это модно. Просто берём название компании, плюс pay, и появляется какой-то новый способ оплаты. Назовем его Вишня Pay. 

Его сделали, маркетинг сказал, как он должен выглядеть. Дизайнеры нарисовали как эта кнопочка должна выглядеть по нашему брендбуку. Теперь нужно, чтобы как можно больше партнёров сразу её везде начали использовать и разместили у себя.

Например, вы вставляете на сайт button. И есть проблема, что стили этого партнёра начинают взаимодействовать с нашей кнопочкой. Как бы мы ни хотели, обвязывались импотантами и делали специфичность классов, мы не можем от этого полностью защититься. Наша кнопочка может просто превратиться в Тыква Pay.

Такая ситуация сейчас популярна на практике. Как это решить, есть несколько вариантов. Самое классическое (например, так работает Яндекс Pay), просто открыть свою кнопочку в iframe. Она оказывается как в аквариуме в безопасности. Все внешние CSS правила идут мимо.

Только вы задаёте через настройки размер закругленности уголков, цвет, тему. Только вы знаете, как она будет выглядеть. Именно так, как нарисовали ваши дизайнеры.

Будущее iframe

Какое будущее может ждать iframe, что у нас есть и к чему готовиться. Есть такая технология, как порталы. 

Порталы

Это не те порталы, что есть в React, и даже не те порталы, что есть в «Рик и Морти». 

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

Они разработали такую технологию как Порталы. Приведу пример, представим себе ситуацию, что вы читаете новости, доходите до конца, и там в виде блоков, расположенных по сетке стоит: «читайте далее». Какие-то следующие статьи. Вы нажимаете на эту следующую статью и всё, она раскрылась.

Правда круто? 

Обратите внимание на адресную строку и на favicon. Человек не тратит время, не ждет, конверсия выше, все максимально гладко. Выглядит это следующим образом. У нас есть тот самый Портал, который заранее загружает этот документ. У него уже всё закешировано. И есть такая функция, как активация этого Портала на то, чтобы он стал основным документом в этом окне. Он просто меняет url в адресной строке. 

На самом деле, там есть немножко магии. Вот пример, как это работает:

// Создаём портал со страницей. Как iframe
// Вы можете использовать тег <portal>

portal = document.createElement("portal");
portal.src = "https://frontendconf.ru/moscow/2022";
portal.style = "...";
document.body.appendChild(portal);

// Когда пользователь нажмёт на превью портала
// Можно вызывать анимацию, например, увеличение портала
// И закончите, выполнив переход

portal.activate();

Вы вставляете Портал в DOM-дерево и ловите событие, кликая на него. Перед тем, как его активировать, вы уже делаете CSS этот transition на всю страницу или как хотите — перевороты и так далее, чтобы это выглядело круто и зрелищно.

Какие у нас остаются вопросы о всех новых технологиях? Какая у них поддержка? Грустная поддержка. Но выглядело вкусно…

Она пока работает только в chrome-подобных браузерах, и сейчас работает под флагами. Это значит, что вам надо зайти по этому пути в браузере и включить пару флажков на то, чтобы это поддерживалось. Переходим в настройки флагов в хроме:

chrome://flags/#enable‐portals

И выбираем следующие флаги

Заключение

Наверное, многие разработчики просто не знали об этом подходе, о том, что так бизнесу можно помогать развиваться. Как адвокат iframe скажу, что не важно, какой у него возраст, он до сих пор остаётся актуальным и в некоторых местах просто незаменимым, чтобы решать какие-то задачи. Он безопасен, если уметь его правильно готовить. А временами iframe — единственный вариант для реализации виджетов.

Комментарии (5)


  1. Ascard
    00.00.0000 00:00

    Про порталы не знал, выглядит круто, спасибо вам! А как там с вёрсткой? Оно же показывается в миниатюре пока не активировано, так? То есть нужно пилить отдельную вёрстку под миниатюру а потом как-то определять активацию, или там всё на media правилах css-а висит? А с трафиком как? Это ж получается что у тебя просто грузится ещё одна страница со всем контентом которую ты не просил и не факт что туда пойдёшь. Ничего не понятно, нужно читать мануалы.


    1. and_kuznetsov Автор
      00.00.0000 00:00

      Порталы сами сделают "пререндер" верстки, т.е. ничего отдельно для миниатюры делать не надо. Я правда проверял только на нормальной классической верстке HTML + CSS. Про верстку, например, на JS (на том же React) самому стало интересно. Как проверю, вернусь – отпишу.

      По поводу трафика – да, заранее грузится страничка "левого" сайта, хоть и пользователь её не просил :) Но сейчас ведь везде так уже. Разработчик подключает не ужатый JS bundle и пользователь качает его, хоть и не просил :))


      1. Ascard
        00.00.0000 00:00

        Пока что мне ясно только то что под портал придётся пилить отдельную уникальную вёрстку. Ведь, если продолжать вашу аналогию с новостями, то что бы оно выглядело прилично, в превьюшке не должно быть всяких блоков меню, шапки, подвала, профиля с аватаркой, своих всплывающих диалогов (например вопросов про печеньки), видео (тем более с автозапуском). И размер такого превьюшки будет меньше чем у самой маленькой media секции. Я уж было порадовался, а тут, после размышлений, вырисовывается геморрой нового порядка.


  1. qertis
    00.00.0000 00:00

    У iFrame существует еще недостаток - у меня события PostMessage режутся на стандартных фильтрах AdBlock и в окружении инкогнито для моего браузера.

    В качестве решения по изолированию стилей, без применения iFrame, неплохо подойдут веб-компоненты. Еще одно решение, делать скоуп стили через атрибут style, - scoped, но это поддерживается сегодня только в браузерах FF.


    1. and_kuznetsov Автор
      00.00.0000 00:00

      Интересно, не встречал проблему с AdBlock... Скорей всего это зависит от формата сообщения, если ваше повторяет Яндекс.Метрику или GA, то возможно, проблема есть.

      Веб-компоненты и в правду хорошая альтернатива, только она еще до сих пор с ограничениями поддерживается в Safari.