Введение в Content Security Policy (CSP)

Submitted by Ромка on Сб, 06/09/2014 - 18:58

Ромка аватар

Content Security Policy
Перевод статьи Майка Веста An Introduction to Content Security Policy от 15 июня 2012 года. Несмотря на то, что статье уже больше 2 лет, информация все еще актуальна и полезна. Об интересном опыте внедрения CSP в Яндексе можно почитать в этой статье.


Модель безопасности в вебе базируется на политике одинакового источника (same origin policy). Только код сайта https://mybank.com должен иметь доступ к данным https://mybank.com, а https://evil.example.com ни при каких условиях не должен получить такого доступа. Каждый источник остается изолированным от остального веба, что дает разработчикам безопасную песочницу, в которой можно разрабатывать и экспериментровать. Теоретически, это бриллиант без изъяна, но на практике, злоумышленники могут найти способы обойти эту систему.

Например, такие атаки как межсайтовый скриптинг (Cross-site scripting, XSS) позволяют обойти политику одного источника, обманным путем заставив сайт доставить вредоносный код вместе легитимным контентом. Это большая проблема, так как браузеры доверяют всему коду, который показывается на странице, так как он является частью страницы доставленной из доверенного источника. XSS Cheat Sheet — это старый, но весьма актуальный список методов, которые могут быть использованы злоумышленниками для внедрения зловредного кода. Если злоумышленнику успешно удается внедрить любой код в страницу, то игру можно считать оконченной: данные сессии пользователя становятся скомпроментированными и информация, которая должна оставаться в секрете, попадает в руки к Плохим Парням™. Мы, конечно же, хотим предотвратить такую возможность.

Этот туториал освящает один многообещающий механизм защиты, который может значительно снизить риск и вред от XSS-атак в современных браузерах — Content Security Policy (CSP).

Белые списки источников

Основная проблема, эксплуатируемая XSS-атакерами, состоит в том, что браузеры не имеют возможности отличить скрипт, являющийся частью вашего приложения, от зловредного скрипта, внедренного третьей стороной. Например, кнопка Google +1 вверху этой статьи (речь о странице с оригинальной статьей) загружает и исполняет код скрипта https://apis.google.com/js/plusone.js в контексте источника текущей страницы. Мы доверяем этому коду, но мы не можем надеяться, что браузер сам догадается, что коду с apis.google.com доверять можно, а коду с apis.evil.example.com — нет. Браузер радостно скачает и исполнит любой код запрошенный страницей, вне зависимости от источника.

Вместо того чтобы слепо доверять всему что доставляется с сервера, CSP определяет HTTP-заголовок Content-Security-Policy, позволяющий создать белый список источников доверенного контента и проинструктировать браузер исполнять код или рендерить ресурсы только из этих источников. Даже если злоумышленник найдет брешь в защите и сможет внедрить скрипт, этот скрипт не будет соответствовать белому списку и не будет исполнен.

Так как мы доверяем apis.google.com, а также самим себе, давайте определим политику, которая позволяет исполнять скрипты только из этих 2 источников:

Content-Security-Policy: script-src 'self' https://apis.google.com

Просто, не правда ли? Как вы наверное догадались, директива script-src контролирует набор привилегий связанных со скриптами на текущей странице. Мы определили 'self' как один разрешенный источник скриптов и https://apis.google.com как второй. Браузер покорно загрузит и исполнит JavaScript с apis.google.com по протоколу HTTPS, а также из источника текущей страницы.

При такой политике браузер будет просто выкидывать ошибку вместо того чтобы загружать скрипт из какого-то незнакомого источника. Когда удачливый злоумышленник сумеет внедрить код в ваш сайт, он получит только следующее сообщение об ошибке, вместо ожидаемого результата:

Console error: "Refused to load the script 'http://evil.example.com/evil.js' because it violates the following Content Security Policy directive: "script-src 'self' https://apis.google.com"."

Политика применима к широкому множеству ресурсов

Несмотря на то, что скрипты — это наиболее очевидный источник рисков в безопасности, CSP предлагает богатый набор директив, которые дают детализированный контроль над ресурсами страницы, которые разрешены для загрузки. Вы уже видели директиву script-src, по этому концепция должна быть вам ясна. Давайте быстренько пройдемся по директивам для других ресурсов:

  • connect-src ограничивает источники, к которым вы можете подключаться (используя XHR, WebSockets и EventSource).
  • font-src определяет источники, которые могут отдавать веб-шрифты. Например разрешить поддержку Google’s Web Fonts можно директивой font-src https://themes.googleusercontent.com.
  • frame-src содержит список источников, для встраиваемых в страницу фреймов. Например: `frame-src https://youtube.com разрешит вставку видео с YouTube, но больше ни из каких других источников.
  • img-src определяет разрешенные источники для загрузки картинок.
  • media-src ограничивает разрешенные источники для доставки аудио и видео .
  • object-src контролирует такие плагины как Flash и другие.
  • style-src это аналог script-src для файлов стилей.

По умолчанию все директивы разрешают загрузку данных откуда угодно. Если вы не установите определенную политику для директивы, к примеру font-src, то это директива будет по умолчанию вести себя так, будто для нее список разрешенных источников определен как * (то есть вы можете загружать шрифты откуда угодно без ограничений).

Вы можете переопределить это поведение по умолчанию, задав значение для директивы default-src. Эта директива, как нетрудно догадаться, задает значения по умолчанию для тех директив, значения которых вы оставили неопределенными. Если default-src установлена в значение https://example.com и вы не определили значение директивы font-src, то это означает, что вы сможете загружать шрифты только с https://example.com и больше ниоткуда. В наших предыдущих примерах мы определяли только значение для script-src, это значит, что картинки, шрифты, и все остальное может загружаться из любых источников.

Вы можете использовать столько директив сколько нужно для вашего приложения просто перечислив в HTTP-заголовке список директив разделенных точкой с запятой. Вы должны помнить, что все источники одного типа должны быть перечислены в одной директиве. Если вы укажете в заголовке что-то вроде script-src https://host1.com; script-src https://host2.com, то вторая директива будет просто проигнорирована. В таком случае заголовок нужно составлять так: script-src https://host1.com https://host2.com.

Если ваше приложение, например, загружает весь свой контент из сети доставки контента (CDN, например https://cdn.example.com) и вам не нужны никакие внешние фреймы и плагины, то ваша политика может выглядеть так:

Content-Security-Policy: default-src https://cdn.example.net; frame-src 'none'; object-src 'none'

Детали реализации


В различных туториалах в интернете вы можете встретить упоминание таких заголовков как X-WebKit-CSP и X-Content-Security-Policy. По хорошему, вам следует игнорировать такие заголовки с префиксами, так как все современные браузеры (за исколючением IE, даже современные 10 и 11 версия поддерживают только кастомные заголовки с префиксами http://caniuse.com/contentsecuritypolicy) поддерживают заголовок Content-Security-Policy, его и нужно использовать.

Вне зависимости от того какой заголовок вы используете, политика определяется для каждой страницы отдельно, то есть вы должны посылать HTTP-заголовки с каждым ответом, для которого хотите обеспечить защиту. Это очень удобно, так как вы можете по разному настраивать политики для разных страниц. Например, на одних страницах вам нужна кнопка +1, а на других — нет, поэтому вы можете разрешать подгрузку её кода только если это необходимо.

Список источников в кажой директиве может быть очень гибко настроен. Вы можете задать в источнике схему (data:, https:), или использовать только диапазон хостов (например, example.com будет соответствовать источнику с любой схемой или портом для указанного хоста), или наоборот полностью указывать URI (https://example.com:443, будет соответствовать только HTTPS, только example.com, и только порту 443). Можно также использовать маски, но только для схемы, порта или крайней левой позиции имени хоста: *://*.example.com:* соответствует всем поддоменам example.com (но не самому example.com), с любой схемой и портом.

Четыре ключевых слова также могут быть использованы в списке источников:

  • 'none', как можно догадаться, соответствует ничему.
  • 'self' соответствует текущему источнику, но не его поддоменам.
  • 'unsafe-inline' разрешает использовать инлайновые JavaScript и CSS (мы поговорим об этом подробно позднее).
  • 'unsafe-eval' позволяет использовать text-to-JavaScript механизмы, такие как eval (эту тему мы тоже затронем позднее).

Эти ключевые слова обязательно должны быть заключены в одинарные кавычки. script-src 'self' разрешает исполнение JavaScript с текущего хоста. script-src self позволяет запускать JavaScript с сервера с именем “self” (а не с текущего хоста), скорее всего это не то чего вы на самом деле хотели.

Sandboxing

Есть еще одна директива, о которой стоит поговорить: sandbox. Она отличается от тех директив, которые мы уже рассмотрели, так как она задает ограничения на действия, которые могут выполняться на странице, а не на ресурсы, которые страница может загружать. Если директива sandbox задана, то страница будет рассматриваться так, будто она загружена внутри iframe с атрибутом sandbox. С помощью этой директивы можно добиться различных эффектов, например, присвоить странице уникальный источник, предотвратить отправку форм и других. Sandboxing выходит за рамки данной статьи, но вы можете найти детальную информацию об этом атрибуте в секции "sandboxing flag set" спецификации HTML5.

Инлайн код опасен


Очевидно, что CSP базируется на белых списках источников, как на однозначном способе сообщить браузеру какие наборы ресурсов использовать разрешено, а какие нет. Однако, белые списки источников не защищают от инлайн скриптов — важнейшей угрозы от XSS-атак. Если атакующему удается внедрить в страницу тэг script со зловредным содержимым (<script>sendMyDataToEvilDotCom();</script>), то у браузера не будет механизма чтобы отличить его от легитимного тэга script. CSP решает эту проблему полным запретом inline-скриптов, это единственный надежный способ решить проблему.

Этот запрет касается не только скриптов вставленных через тэг script, но и иналайновых обработчиков событий и javascript в ссылках. Вам нужно будет переместить контент тэгов script во внешний файл и заменить javascript вида <a ... onclick="[JAVASCRIPT]"> на соответствующие вызовы addEventListener. Например, такой скрипт:

<script>
  function doAmazingThings() {
    alert('YOU AM AMAZING!');
  }
</script>
<button onclick='doAmazingThings();'>Am I amazing?</button>

вы можете переписать так:

<!-- amazing.html -->
<script src='amazing.js'></script>
<button id='amazing'>Am I amazing?</button>
// amazing.js
function doAmazingThings() {
  alert('YOU AM AMAZING!');
}
document.addEventListener('DOMContentReady', function () {
  document.getElementById('amazing')
          .addEventListener('click', doAmazingThings);
});

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

Инлайновые стили имеют те же недостатки: стили должны быть вынесены во внешние файлы, чтобы защититься от разнообразных сюрпризов связанных с продвинутыми методами фильтраци (exfiltration methods), поддерживающимися в CSS.

Если вам действительно нужны инлайновые скрипты и стили, то вы можете разрешить их использование добавив 'unsafe-inline' в список разрешенных источников в директивы script-src и style-src. Но, пожалуйста, не делайте этого. Запрет инлайн скриптов это важнейшая мера защиты, предоставляемая CSP. Запрет инлайн стилей также делает ваше приложение более надежным. Потребуется немного усилий для избавления от инлайн скриптов, но эффект будет значительным.

и eval тоже

Даже если атакующий не может внедрить скрипт в вашу страницу, он имеет возможность обмануть ваше приложение и заставить его конвертировать обычный текст в JavaScript и исполнить его. eval(), new Function(), setTimeout([string], ...), и setInterval([string], ...) — это все векторы атаки, которые могут быть использованы для исполнения зловредного кода. CSP, по умолчанию, понимает этот риск и, что логично, блокирует все эти вектора разом.

Этот факт вилияет на то, как вам следует разрабатывать свои приложения:

  • JSON нужно парсить через встроенный метод JSON.parse, а не через eval. Нативная поддержка JSON есть во всех современных браузерах со времен IE8, и она абсолютно безопасна.
  • Скрипты использующие вызовы setTimeout и setInterval должны содержать инлайновые функции, а не строки. Например, такой код:
setTimeout("document.querySelector('a').style.display = 'none';", 10);

нужно переписать так:

setTimeout(function () {
  document.querySelector('a').style.display = 'none';
}, 10);
  • Избегайте инлайнового рендеринга шаблонов в рантайме: многие библиотеки-шаблонизаторы свободно используют new Function() для увеличения скорости генерации шаблонов. Это изящный подход, но он несет в себе риск исполнения зловредного текста. Некоторые фреймворки поддерживают CSP из коробки и используют надежные парсеры при отсутствии eval. Хороший пример этого — директива ng-csp в фреймворке AngularJS.

Однако, даже лучше если ваш шаблонный движок имеет возможность прекомпиляции (Handlebars, например, имеет такую возможность). Прекомпиляция ваших шаблонов может сделать ваше приложениее даже быстрее, быстрее чем рантайм реализации, и это также безопасный подход. Ура, ура! Если eval и его text-to-JavaScript соратники принципиально важны для вашего приложения, то вы можете активировать их поддержку добавив 'unsafe-eval' как разрешенный ресурс в директиву script-src. Но, еще раз прошу, пожалуйста, не делайте этого. Запрет возможности исполнять строки очень сильно усложняет атакующему найти способ исполнить неавторизованный скрипт на вашем сайте.

Отчеты


CSP позволяет блокировать недовренные ресурсы на клиенте, это большой плюс для ваших посетителей. Но было бы полезно иметь возможность отправлять на сервер уведомления, которые помогли бы идентифицировать и исправить (в оригинале — расплющить) любые баги, которые позволяют внедрить зловредный код. Чтобы сделать это, вы можете проинструктировать браузер отправлять методом POST специальный отчет в формате JSON на адрес определенный в директиве report-uri.
Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

Такой отчет будет выглядеть следующим образом:

{
  "csp-report": {
    "document-uri": "http://example.org/page.html",
    "referrer": "http://evil.example.com/",
    "blocked-uri": "http://evil.example.com/evil.js",
    "violated-directive": "script-src 'self' https://apis.google.com",
    "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
  }
}

Он содержит достаточно информации, чтобы вы могли разобраться в причинах уязвимости, включая адрес уязвимой страницы (document-uri), referrer страницы (обратите внимание, в слове referrer нет ошибки), ресурс, который нарушил политику страницы (blocked-uri), нарушенная директива (violated-directive), и политика примененная на странице (original-policy).

Report-Only

Если вы только начинаете использовать CSP, то важно проверить текущее состояние вашего приложения, прежде чем выкатывать драконовские политики. Перед окончательной выкладкой политик в продакшен вы можете попросить браузер уведомлять об уязвимостях, но не применять ограничения. Для этого вместо заголовка Content-Security-Policy используйте Content-Security-Policy-Report-Only:

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

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

Практические примеры использования

CSP поддерживается в Chrome 16+, Safari 6+, Opera 23+ и Firefox 4+, а также имеет очень ограниченную поддержку в IE 10. Большие сайты типа Twitter и Facebook уже используют CSP (опыт Twitter рекомендован к прочтению) и стандарт уже готов к тому, чтобы использовать его и на вашем сайте.

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

Пример #1: виджеты социальных кнопок

Кнопка Google +1 использует скрипт с https://apis.google.com и встраивает iframe с https://plusone.google.com. Вам понадобится политика, которая разрешает использование этих двух источников. Минимальная политика будет такй: script-src https://apis.google.com; frame-src https://plusone.google.com. Вам также надо будет убедиться, что JavaScript-сниппет, предоставляемый Гуглом также вынесен в отдельный файл.

Кнопка Like Фейсбука имеет несколько реализаций, я рекомендую остановиться на варианте с iframe, так как эта кнопка работает в безопасной песочнице по отношению к вашему сайту. Такая кнопка потребует использования директивы frame-src https://facebook.com. Учтите, что по умолчанию код айфрейма Фейсбука содержит относительный URL вида //facebook.com. Замените его на явный вариант с HTTPS: https://facebook.com. Нет причин использовать HTTP-версию кнопки.

Кнопка Tweet Твиттера бизируется на скрипте и фрейме, которые загружаются с https://platform.twitter.com (Твиттер также по умолчанию использует относительный URL, не забудьте отредактировать код и определить в нем использование HTTPS прежде чем вставлять код на сайт). Вам нужно будет установить script-src https://platform.twitter.com; frame-src https://platform.twitter.com и переместить JavaScript-сниппет во внешний файл.

Остальные платформы имеют похожие коды и вы можете настроить их аналогичным образом. Я рекомендую установить директиве default-src значение 'none' и изучать сообщения в консоли, котороые помогут определить вам список ресурсов, которые вам необходимо включить, чтобы виджеты заработали.

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

script-src https://apis.google.com https://platform.twitter.com; frame-src https://plusone.google.com https://facebook.com https://platform.twitter.com

Пример #2: блокировка

Предположим, что вы разрабатываете сайт для банка и хотите быть уверенными в том, что загужаются только те ресурсы, которые вы создали сами. В этом случае следует начать с того, что установить политику по умолчанию на полную блокировку (default-src 'none').

Теперь предположим, что сайт загружает картинки, стили и скрипты с CDN https://cdn.mybank.net, а также посылает аякс-запросы к https://api.mybank.com/. Фреймы используются, но только для локальных страниц (никаких внешних источников). На сайте нет Flash, внешних скриптов и чего бы то ни было подобного. Самый ограничивающий заголовок, который мы могли бы использовать в таком случае будет выглядеть так:

Content-Security-Policy: default-src 'none'; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net; img-src https://cdn.mybank.net; connect-src https://api.mybank.com; frame-src 'self'

Пример #3: только SSL

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

Content-Security-Policy: default-src https:; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'

Несмотря на то что https: определн в директиве default-src, директивы script-src и style-src не наследуют этот параметр, а полностью перезаписывают дефолтную директиву. По этому в каждой из этих директив нужно полностью указывать все необходимые параметры (в том числе и https: в нашем случае).

Будущее



Content Security Policy 1.0 находится в статусе W3C Candidate Recommendation и разработчики браузеров активно внедряют поддержку стандарта. Рабочая группа W3C Web Application Security не сидит без дела и работает над черновиком следующей версии спецификации. Редакторский черновик Content Security Policy 1.1 находится в активной разработке (комментарий переводчика — на данный момент доступен черновик Content Security Policy Level 2).

CSP 1.1 содержит несколько интересных нововведений коротко описанных ниже.

  • Поддержка инлайн скриптов и стилей. Выше мы говорили, что лучше избегать использования инлайн скриптов и стилей. Спецификация версии 1.1 позволит разработчикам создавать белые списки инлайн-блоков с помощью временных значений (nonce) или хэшей. Конкретный синтаксис пока в разработке.

  • Внедрение политик с помощью мета-тэгов. На данный момент в CSP используется механизм доставки политик с помощью HTTP-заголовков. Однако, может быть удобно внедрять описание политик непосредственно в разметку документа или объявлять ее через скрипт. На тему того следует ли внедрять такие возможности идут оживленные споры, а браузеры базирующиеся на WebKit (например Chrome) уже поддерживают объявление политик через мета-тэг: добавьте <meta http-equiv="Content-Security-Policy" content="[POLICY GOES HERE]"> в head вашего документа и испытайте эту возможность.

    Вы также можете внедрить политику в рантайме добавив мета-тэг через скрипт. Удачным первым шагом на пути к полностью блокированному приложению может быть внедрение политик в вашем приложении после того как все необходимые ресурсы загружены. Это сделает ваш сайт относительно безопасным (хотя все еще остается большой риск атак на этапе загрузки, пока политики не активированы), но уже позволяет вам использовать преимущества CSP перед миграцией к использованию HTTP-заголовка.

  • DOM API: если эта фича будет принята в следующей версии CSP, то появится возможность запросить у страницы текущую политику через JavaScript, что позволит вам в рантайме принимать решения о том как правильнее реализовать ту или иную сособенность. Если, например, доступно использование eval(), то ваш код может реализовать определенную фичу иным способом, чем в случае если eval() недоступен. Это может быть полезно для разработчиков фреймворков. Спецификация API находится в разработке и вам следует следить за обновлениями её черновика.

  • Новые директивы: несколько новых директив также находятся в обсуждении, например директива plugin-types, которая может ограничивать MIME-типы контента, плагины для которых могут быть загружены; form-action ограничивает отправку форм только в определенные источники; и несколько других, которые пока окончательно не определены.

Если вам интересны обсуждения на эту тему, просмотрите архивы рассылки public-webappsec@w3.org или подпишитесь на неё.