Безопасное воспроизведение в изолированных фреймах iframe

Создание современного сайта почти неизбежно включает в себя встраивание компонентов и контента, которые вы не можете контролировать. Сторонние виджеты могут повысить вовлеченность пользователей и сыграть важную роль в их взаимодействии с сайтом. Иногда контент, созданный пользователями, даже важнее, чем собственный контент сайта. Отказываться от них не стоит, но и те и другие повышают риск того, что на вашем сайте произойдет Что-то Плохое™. Каждый виджет, который вы встраиваете, – будь то реклама или виджет социальной сети, – это потенциальный вектор атаки для злоумышленников.

Content Security Policy (CSP) позволяет снизить риски, связанные с обоими типами контента, за счет создания белого списка надежных источников скриптов и другого контента. Это важный шаг в правильном направлении, но стоит отметить, что защита, которую предлагает большинство директив CSP, является бинарной: ресурс либо разрешен, либо нет. Иногда бывает полезно сказать: "Я не уверен, что доверяю этому источнику контента, но он такой красивый! Встрой его, пожалуйста, браузер, но не дай ему сломать мой сайт".

Минимальные привилегии

По сути, мы ищем механизм, который позволит нам предоставлять контенту, который мы встраиваем, только минимальный уровень возможностей, необходимых для выполнения его работы. Если виджету не нужно открывать новое окно, то запрет на использование window.open не причинит вреда. Если нет, то отключение поддержки плагинов не должно вызвать проблем. Мы можем обеспечить максимальную безопасность, если будем следовать принципу минимальных привилегий и блокировать все функции, которые не имеют прямого отношения к нужной нам функциональности. В результате нам больше не нужно слепо доверять тому, что какой-то встроенный контент не воспользуется привилегиями, которые он не должен использовать. У него просто не будет доступа к этой функции.

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

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

Доверяй, но проверяй

Кнопка "Твитнуть" – отличный пример функции, которую можно безопасно встроить на сайт с помощью изолированной среды. Twitter позволяет встроить кнопку с помощью iframe, используя следующий код:

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

Чтобы понять, что мы можем заблокировать, давайте внимательно изучим, какие возможности требуются кнопке. HTML-код, загруженный во фрейм, выполняет небольшой фрагмент кода JavaScript с серверов Twitter и при нажатии генерирует всплывающее окно с интерфейсом для публикации твитов. Для этого интерфейсу нужен доступ к файлам cookie Twitter, чтобы связать твит с нужным аккаунтом, и возможность отправлять форму твита. Вот и все. Фрейму не нужно загружать плагины, переходить в окно верхнего уровня или выполнять другие действия. Поскольку ему не нужны эти привилегии, давайте удалим их, изолировав содержимое фрейма.

Песочница работает на основе белого списка. Сначала мы удаляем все возможные разрешения, а затем снова включаем отдельные возможности, добавляя определенные флаги в конфигурацию песочницы. Для виджета Twitter мы решили включить JavaScript, всплывающие окна, отправку форм и файлы cookie twitter.com. Для этого добавим атрибут sandbox в тег iframe со следующим значением:

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

Вот и всё! Мы предоставили фрейму все необходимые возможности, и браузер будет отказывать ему в доступе к любым привилегиям, которые мы явно не предоставили через значение атрибута sandbox.

Точный контроль над возможностями

В примере выше мы видели несколько возможных флагов изолированной среды. Теперь давайте подробнее рассмотрим, как работает атрибут.

Если у iframe пустой атрибут sandbox, то документ во фрейме будет полностью изолирован и на него будут наложены следующие ограничения:

  • JavaScript не будет выполняться в документе, размещенном во фрейме. Это относится не только к коду JavaScript, загружаемому с помощью тегов script, но и к встроенным обработчикам событий и URL-адресам javascript:. Кроме того, будет показан контент, содержащийся в тегах noscript, как если бы пользователь сам отключил скрипты.
  • Документ во фрейме загружается в уникальный источник, а значит, все проверки на совпадение источников будут завершаться неудачно. Уникальные источники не совпадают ни с какими другими, даже с самими собой. В частности, это означает, что у документа нет доступа к данным, хранящимся в файлах cookie любого источника или в любых других механизмах хранения (хранилище DOM, Indexed DB и т. д.).
  • Документ во фрейме не может создавать новые окна или диалоговые окна (например, с помощью window.open или target="_blank").
  • Не удалось отправить форму.
  • Плагины не будут загружаться.
  • Документ во фрейме может управлять только собой, но не родительским элементом верхнего уровня. Если задать window.top.location, будет выдано исключение, а нажатие на ссылку с target="_top" ни к чему не приведет.
  • Функции, которые запускаются автоматически (например, элементы формы с автоматической фокусировкой, автоматическое воспроизведение видео и т. д.), блокируются.
  • Не удалось заблокировать указатель.
  • Атрибут seamless игнорируется в элементе iframes, который содержится в документе во фрейме.

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

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

  • allow-forms разрешает отправку формы.
  • allow-popups разрешает всплывающие окна.
  • allow-pointer-lock позволяет блокировать указатель.
  • allow-same-origin позволяет документу сохранить свой источник. Страницы, загруженные из https://example.com/, сохранят доступ к данным этого источника.
  • allow-scripts разрешает выполнение JavaScript, а также позволяет функциям запускаться автоматически (поскольку их легко реализовать с помощью JavaScript).
  • allow-top-navigation позволяет документу выйти за пределы фрейма, перейдя в окно верхнего уровня.

Теперь мы можем точно определить, почему в примере с Twitter выше мы получили именно такой набор флагов:

  • allow-scripts требуется, поскольку страница, загруженная во фрейм, выполняет код JavaScript для обработки взаимодействия с пользователем.
  • allow-popups требуется, так как кнопка открывает форму для твита в новом окне.
  • allow-forms – обязательный атрибут, поскольку форма для твитов должна быть доступна для отправки.
  • allow-same-origin необходимо, так как в противном случае файлы cookie сайта twitter.com будут недоступны и пользователь не сможет войти в систему, чтобы отправить форму.

Важно отметить, что флаги изолированной среды, примененные к фрейму, также применяются к любым окнам или фреймам, созданным в изолированной среде. Это означает, что мы должны добавить allow-forms в изолированную среду фрейма, даже если форма существует только в окне, которое открывает фрейм.

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

Разделение привилегий

Изоляция стороннего контента в песочнице для запуска ненадежного кода в среде с низкими привилегиями дает очевидные преимущества. Но как насчет вашего собственного кода? Вы доверяете себе, верно? Зачем нужна тестовая среда?

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

Чтобы ещё больше снизить риск, разделите приложение на логические части и поместите каждую из них в изолированную среду с минимальными привилегиями. Этот метод очень распространен в нативном коде. Например, Chrome разделяет себя на процесс браузера с высоким уровнем привилегий, который имеет доступ к локальному жесткому диску и может устанавливать сетевые подключения, и множество процессов рендеринга с низким уровнем привилегий, которые выполняют тяжелую работу по синтаксическому анализу ненадежного контента. Рендерерам не нужно прикасаться к диску, браузер заботится о том, чтобы предоставить им всю информацию, необходимую для рендеринга страницы. Даже если злоумышленник найдет способ взломать модуль визуализации, он не сможет причинить серьезного вреда, поскольку модуль визуализации не может выполнять никаких действий самостоятельно. Все операции, требующие повышенных привилегий, должны выполняться через процесс браузера. Чтобы нанести ущерб, злоумышленникам придется найти несколько уязвимостей в разных частях системы, что значительно снижает риск успешной атаки.

Безопасная изолированная среда eval()

Благодаря изолированной среде и postMessage API эту модель можно легко применить к веб-ресурсам. Части приложения могут находиться в изолированных элементах iframe, а родительский документ может выступать в качестве посредника между ними, отправляя сообщения и ожидая ответов. Такая структура гарантирует, что уязвимости в одной части приложения нанесут минимальный ущерб. Кроме того, вам придется создать четкие точки интеграции, чтобы вы точно знали, где нужно быть осторожным при проверке ввода и вывода. Давайте рассмотрим простой пример, чтобы понять, как это работает.

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

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

Внутри фрейма находится минимальный документ, который просто прослушивает сообщения от родительского документа, подключаясь к событию message объекта window. Когда родительский фрейм выполняет postMessage для содержимого iframe, активируется это событие, предоставляя нам доступ к строке, которую родительский фрейм хочет выполнить.

В обработчике мы получаем атрибут source события, который является родительским окном. Мы используем его, чтобы отправить результат нашей работы, когда закончим. Затем мы сделаем самое сложное – передадим полученные данные в eval(). Этот вызов заключен в блок try, поскольку запрещенные операции в изолированном элементе iframe часто генерируют исключения DOM. Мы перехватим их и покажем понятное сообщение об ошибке. Наконец, мы отправляем результат обратно в родительское окно. Это довольно просто.

Родительский элемент устроен так же просто. Мы создадим небольшой интерфейс с элементом textarea для кода и элементом button для выполнения, а также добавим frame.html через изолированный элемент iframe, разрешив только выполнение скрипта:

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

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

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

Теперь подключим обработчик событий к кликам по элементу button. Когда пользователь нажимает кнопку, мы берем текущее содержимое элемента textarea и передаем его во фрейм для выполнения:

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

Правда просто? Мы создали очень простой API для оценки и можем быть уверены, что оцениваемый код не имеет доступа к конфиденциальной информации, такой как файлы cookie или хранилище DOM. Аналогично, оцениваемый код не может загружать плагины, открывать новые окна или выполнять другие раздражающие или вредоносные действия.

Вы можете сделать то же самое со своим кодом, разбив монолитные приложения на одноцелевые компоненты. Каждый из них может быть обернут в простой API обмена сообщениями, как мы написали выше. Родительское окно с высокими привилегиями может действовать как контроллер и диспетчер, отправляя сообщения в определенные модули, каждый из которых имеет наименьшие возможные привилегии для выполнения своей работы, прослушивая результаты и гарантируя, что каждый модуль хорошо снабжен только той информацией, которая ему требуется.

Однако будьте осторожны, если фрейм и родительский элемент имеют один и тот же источник. Если страница на сайте https://example.com/ содержит фрейм с другой страницей того же источника, в котором есть атрибут sandbox с флагами allow-same-origin и allow-scripts, то страница во фрейме может получить доступ к родительской странице и полностью удалить атрибут sandbox.

Играйте в песочнице

Изолированная среда доступна в различных браузерах: Firefox 17+, IE10+ и Chrome (актуальная таблица поддержки). Применяя атрибут sandbox к элементу iframes, вы можете предоставить определенные права доступа к контенту, который в нем показывается. При этом следует предоставлять только те права, которые необходимы для корректной работы контента. Это позволяет снизить риски, связанные с добавлением стороннего контента, в дополнение к возможностям, которые уже доступны в Content Security Policy.

Кроме того, песочница – это мощный инструмент, который позволяет снизить риск того, что злоумышленник сможет воспользоваться уязвимостями в вашем коде. Разделив монолитное приложение на набор изолированных сервисов, каждый из которых отвечает за небольшой фрагмент автономной функциональности, злоумышленники будут вынуждены не только скомпрометировать содержимое определенных фреймов, но и их контроллер. Это гораздо более сложная задача, особенно потому, что контроллер может быть значительно уменьшен в объеме. Вы можете потратить свои усилия, связанные с безопасностью, на аудит этого кода, если попросите браузер помочь с остальным.

Это не значит, что песочница – идеальное решение проблемы безопасности в интернете. Она обеспечивает углубленную защиту, и если вы не контролируете клиентские устройства пользователей, то не можете полагаться на поддержку браузеров для всех пользователей (если вы контролируете клиентские устройства пользователей, например в корпоративной среде, то это отлично). Когда-нибудь… но пока песочница – это ещё один уровень защиты, а не полноценная защита, на которую можно полностью положиться. Тем не менее, слои отличные. Я предлагаю использовать этот.

Дополнительные сведения

  • В интересной статье Privilege Separation in HTML5 Applications (Разделение привилегий в приложениях HTML5) описывается разработка небольшого фреймворка и его применение в трех существующих приложениях HTML5.

  • Изолированная среда может быть ещё более гибкой, если использовать ее в сочетании с двумя другими новыми атрибутами iframe: srcdoc и seamless. Первый позволяет заполнить фрейм контентом без дополнительных затрат на HTTP-запрос, а второй позволяет стилю перетекать в контент фрейма. Оба они в настоящее время имеют довольно слабую поддержку браузеров (Chrome и WebKit nightlies), но в будущем это будет интересная комбинация. Например, вы можете изолировать комментарии к статье с помощью следующего кода:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>