Играйте безопасно в изолированных IFrames

Создание богатого опыта в современной сети почти неизбежно предполагает встраивание компонентов и контента, над которыми вы не имеете реального контроля. Сторонние виджеты могут стимулировать взаимодействие и играть решающую роль в общем пользовательском опыте, а пользовательский контент иногда даже более важен, чем собственный контент сайта. Воздерживаться от любого из них на самом деле не вариант, но и то, и другое увеличивает риск того, что на вашем сайте может произойти что-то плохое™. Каждый виджет, который вы встраиваете — каждое объявление, каждый виджет социальных сетей — является потенциальным вектором атаки для злонамеренных лиц:

Политика безопасности контента (CSP) может снизить риски, связанные с обоими этими типами контента, предоставляя вам возможность внести в белый список специально доверенные источники сценариев и другого контента. Это важный шаг в правильном направлении, но стоит отметить, что защита, которую предлагает большинство директив CSP, является двоичной: ресурс разрешен или нет. Бывают случаи, когда было бы полезно сказать: «Я не уверен, что действительно доверяю этому источнику контента, но он ооочень красивый! Встройте его, пожалуйста, Браузер, но не позволяйте ему сломать мой сайт».

Наименьшие привилегии

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

Элементы 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 мы решили включить JavaScript, всплывающие окна, отправку форм и файлы cookie twitter.com. Мы можем сделать это, добавив в iframe атрибут sandbox со следующим значением:

<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 с пустым атрибутом «песочницы», документ в рамке будет полностью помещен в «песочницу», подвергаясь следующим ограничениям:

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

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

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

  • 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()

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

Evalbox — замечательное приложение, которое принимает строку и обрабатывает ее как JavaScript. Вау, правда? Именно то, чего вы ждали все эти долгие годы. Конечно, это довольно опасное приложение, поскольку разрешение выполнения произвольного JavaScript означает, что любые данные, которые может предложить источник, доступны для захвата. Мы снизим риск возникновения Bad Things™, гарантируя, что код выполняется внутри песочницы, что делает его немного безопаснее. Мы проработаем код изнутри наружу, начиная с содержимого фрейма:

<!-- 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/ обрамляет другую страницу того же источника с помощью песочницы, включающей флагиallow -same-origin иallow-scripts , то страница с фреймом может обратиться к родительскому элементу и удалить его. атрибут песочницы полностью.

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

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

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

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

Дальнейшее чтение

  • « Разделение привилегий в приложениях HTML5 » — интересная статья, в которой рассматривается разработка небольшой платформы и ее применение к трем существующим приложениям HTML5.

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

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