Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Кросс-доменные ограничения

Оригинал:The Same-Origin Policy
Авторы: Eunsuk Kang, Santiago Perez De Rosso, and Daniel Jackson,
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: январь 2017 г.

Это шестая часть статьи "Кросс-доменные ограничения".
Перейти к
началу статьи.

Техника обхода кросс-доменных ограничений

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

В этом разделе мы рассмотрим четыре метода, которые были разработаны веб-разработчиками и часто ими используются для того, чтобы обойти ограничения, накладываемые кросс-доменными ограничениями: 1) свойство document.domain, 2) метод JSONP, 3) метод PostMessage и 4) кросс-доменное использование ресурсов. Они являются ценными методами, но если ими пользоваться без осторожности, то можно сделать веб-приложение уязвимым для именно таких видов атак, для предотвращения которых были в первую очередь разработаны кросс-доменные ограничения.

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

check Confidentiality for 5
check Integrity for 5

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

Свойство document.domain

Первым в нашем списке методов мы рассмотрим использование свойства document.domain в качестве способа обхода кросс-доменных ограничений. Идея этого метода заключается в том, чтобы благодаря тому, что значение свойства document.domain у двух документов, скачанным из разных источников, устанавливается одинаковым, позволить этим двум документам получать доступ к DOM друг друга. Так, например, в случае, если скрипты в обоих документах устанавливают свойство document.domain равным example.com (мы предполагаем, что оба адреса URL используют один и тот же протокол и порт), скрипт из email.example.com может читать DOM документа, взятого из calendar.example.com, и делать в него записи.

Мы моделируем поведение настройки свойства document.domain указывая его как тип операции браузера с названием SetDomain:

// Модификация свойства document.domain
sig SetDomain extends BrowserOp { newDomain: Domain }{
  doc = from.context
  domain.end = domain.start ++ doc -> newDomain
  -- не изменяет содержимое документа
  content.end = content.start
}

Поле newDomain представляет собой значение, которое должно быть установлено для свойства. Хотя есть один нюанс: сценарии могут установить свойство домена только для правильного полностью квалифицированного фрагмента имени его хоста. Например, email.example.com можно изменить на example.com, но не на google.com. Мы используем fact, в котором формулируется правило для поддоменов:

// Скрипт может установить свойство domain только для элемента, указанного в правой части,
// предствляющего собой польностю кевалифицированный фрагмент имени хоста
fact setDomainRule {
  all d: Document | d.src.host in (d.domain.Time).subsumes
}

Если бы не было этого правила, то любой сайт мог устанавливать любое значение для свойства document.domain, что означало, что, например, злонамеренный сайт может установить для свойства domain значение, взятое из домена вашего банка, загрузить в iframe ваш банковский счет и (если предположить, что страница банка имеет точно такую же настройку свойства domain) прочитать DOM страницы вашего банка.

Давайте вернемся к нашему первоначальному определению кросс-доменного ограничения и ослабим ограничение доступа к DOM с тем, чтобы учесть влияние свойства document.domain. Если два скрипта устанавливают это свойство равным одному и тому же значению, и они используют один и тот же протокол и порт, то эти два скрипта могут взаимодействовать друг с другом (то есть, читать и писать DOM друг друга).

fact domSop {
  -- Для каждой успешной операции чтения/записи DOM,
  all o: ReadDom + WriteDom |  let target = o.doc, caller = o.from.context |
    -- (1) документы target и caller из того же самого источника, либо
    origin[target] = origin[caller] or
    -- (2) свойства domain обоих документов должны быть изменены
    (target + caller in (o.prevs <: SetDomain).doc and
      -- ...и они должны соответствовать значениям источника.
      currOrigin[target, o.start] = currOrigin[caller, o.start])
}

Здесь currOrigin[d, t] является функцией, которая возвращает в качестве имени хоста документа d его происхождение с его свойством document.domain в момент времени t.

Стоит отметить, что свойства document.domain для обоих документов должны быть установлены явно через некоторое время после их загрузки в браузер. Будем говорить, что документ А загружается изexample.com, а у документа B, загруженного из calendar.example.com, есть его свойство domain, измененное на example.com. Даже если теперь эти два документа имеют одинаковое значение свойства domain, они не смогут взаимодействовать друг с другом в случае, если документ А также явно не установит значение своего свойства равным example.com. Во-первых, такое поведение кажется довольно странным. Тем не менее, без него могут произойти разные неприятности. Например, сайт может быть подвергнут межсайтовой скриптовой атаки из своих поддоменов: вредоносный скрипт в документе B может изменить свое свойство domain на example.com и манипулировать DOM в документе А, хотя последний никогда не намеревался взаимодействовать с документом B.

Анализ: Теперь, когда мы ослабили кросс-доменные ограничения для того, чтобы позволить кросс-доменное взаимодействие при определенных обстоятельствах, есть ли гарантия того, что кросс-доменные ограничения все еще обеспечивают безопасность? Давайте попросим анализатор языка Alloy сказать нам, может ли атакующий злоупотребить свойство document.domain для получения доступа или подделать конфиденциальные данные пользователя.

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

check Confidentiality for 5

Этот сценарий состоит из пяти шагов; первые три шага показывают типичное использование свойства document.domain, когда два документа CalendarPage и InboxPage, полученные из различных источников, взаимодействуют друг с другом благодаря тому, что значение свойства domain у них общее (ExampleDomain). На последних двух шагах вводится еще один документ, BlogPage, который был скомпрометирован вредоносным скриптом, пытающимся получить доступ к содержимому двух других документов.

В начале сценария (рис.17.7 и рис.17.8) документы InboxPage и CalendarPage имеют свойство domain с двумя различными значениями (EmailDomain и ExampleDomain, соответственно), так что браузер будет препятствовать им получить доступ к DOM друг друга. Скрипты, запускаемые внутри документов (InboxScript и CalendarScript) каждый выполняет операцию SetDomain для изменения своих собственных свойств domain на значение ExampleDomain (доптимо, поскольку ExampleDomain является супердоменом исходного домена).

Рис.17.7. Кросс-доменный контрпример в момент времени 0

Рис.17.8. Кросс-доменный контрпример в момент времени 1

Сделав это, они теперь могут получить доступ к DOM друг друга и выполнять операции ReadDom или WriteDom так, как это показано на рис.17.9.

Рис.17.9. Кросс-доменный контрпример в момент времени 2

Обратите внимание, что когда вы устанавливаете значение example.com для email.example.com и calendar.example.com, вы разрешаете этим двум страницам общаться не только друг с другом, но и с любой другой страницей (например, с blog.example.com), у которой супердомен задан как example.com. Атакующий также знает об этом и создает специальный скрипт (EvilScript), который работает внутри страницы блога атакующего (BlogPage). На следующем шаге (рис.17.10), скрипт выполняет операцию SetDomain для того, чтобы изменить значение свойства domain для страницы BlogPage на ExampleDomain.

Рис.17.10. Кросс-доменный контрпример в момент времени 3

Теперь, когда у страницы BlogPage значение свойства domain точно такое же, как и у двух других документов, она может успешно выполнять операцию ReadDOM для доступа к содержимому этих других документов (рис.17.11).

Рис.17.11. Кросс-доменный контрпример в момент времени 4

Эта атака указывает на один важный недостаток метода с использованием свойства domain для кросс-доменных взаимодействий: Безопасность приложения, использующего этот метод, настолько сильна, насколько сильна безопасность самого слабого звена из всех страниц, которые пользуются одним и тем же базовым доменом. Вскоре мы обсудим другой метод, называемый PostMessage, который можно использовать для более общего класса кросс-доменных взаимодействий и который в то же время является более безопасным.

Метод JSON с оберткой (JSONP)

Перед тем, как был предложен метод кросс-доменное использование ресурсов (который мы рассмотрим в ближайшее время), метод JSONP был, пожалуй, самым популярным методом для обхода кросс-доменных ограничений в запросах XMLHttpRequest, причем и сегодня он используется довольно широко. Метод JSONP основан на том, что теги в HTML, в которых есть скрипт (т. е. тег <script>) освобождаются от проверки кросс-доменных ограничений*; то есть, вы можете взять скрипт из любого адреса URL и браузер легко выполняет его в текущем документе:

(*Без этого нельзя было бы загружать библиотеки JavaScript, такие как JQuery, из других доменов).

<script src="/images/BOOKS/AOSA/17/http://www.example.com/myscript.js"></script>

Тег скрипта можно использовать для получения кода, но как сделать, чтобы мы могли использовать его для получения произвольных данных (например, объекта в формате JSON) из другого домена? Проблема заключается в том, что браузер ожидает, что содержимое src будет фрагментом кода на языке JavaScript, а поскольку он просто хранит его указатель на источник данных (например, на файл JSON или HTML), то это приводит к синтаксической ошибке.

Один из способов состоит в том, чтобы поместить обернуть нужные данные внутрь строки, которую браузер распознает как действительный код на языке JavaScript; эта строка иногда называется оберткой или padding (отсюда и название "JSON with padding" - "JSON с оберткой"). Такая обертка может может быть любым произвольным кодом на языке JavaScript, но обычно, это имя функции обратного вызова (уже определенной в настоящем документе), который должен быть с возвращенными в ответ данными:

<script src="/images/BOOKS/AOSA/17/http://www.example.com/mydata?jsonp=processData"></script>

Сервер на www.example.com распознает его как запрос JSONP и обвертывает запрашиваемые данные внутри параметра jsonp:

processData(mydata)

который является корректной инструкцией на языке JavaScript (а именно, применение функции "processData" к значению "mydata") и выполняется браузером в текущем документе.

В нашей модели JSONP моделируется как вариант запроса HTTP, котором указан идентификатор функции обратного вызова в поле padding. После получения запроса JSONP, сервер возвращает ответ, в котором находится запрашиваемый ресурс (payload), обернутый в функцию обратного вызова (cb).

sig CallbackID {}  // идентификат функции обратного вызова
// Запрос посылается как результат интерпретации тега <script> tag
sig JsonpRequest in BrowserHttpRequest {
  padding: CallbackID
}{
  response in JsonpResponse
}
sig JsonpResponse in Resource {
  cb: CallbackID,
  payload: Resource
}

Когда браузер получает ответ, он выполняет функцию обратного вызова с данными payload:

sig JsonpCallback extends EventHandler {
  cb: CallbackID,
  payload: Resource
}{
  causedBy in JsonpRequest
  let resp = causedBy.response | 
    cb = resp.@cb and
    -- результат запроса JSONP передается в функцию обратного вызова как аргумент
    payload = resp.@payload
}

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

Обратите внимание, что функция обратного вызова выполняется так же, как и та, который включена в ответ (cb = resp.@cb), но она не обязательно имеет ту же самую обертку как в исходном запросе JSONP. Другими словами, чтобы взаимодействие с помощью JSONP работало, сервер будет отвечать за правильное построение ответа, в котором будет исходная обертка в виде функции обратного вызова (т.е. гарантируется, что JsonRequest.padding = JsonpResponse.cb). В принципе, сервер может выбрать любую функцию обратного вызова (или любой фрагмент кода на языке JavaScript), в том числе и тот, который не имеет ничего общего с padding, имеющимся в запросе. Это подчеркивает потенциальный риск использования метода JSONP: сервер, который принимает запросы JSONP, должен быть надежным и безопасным, поскольку на нем есть возможность выполнить в клиентском документе любой фрагмент кода на языке JavaScript.

Анализ: Проверка свойства Confidentiality с помощью анализатора языка Alloy возвращает контрпример, в котором показывается один потенциальный риск безопасности при использовании метода JSONP. В этом сценарии приложение календаря (CalendarServer) делает свои ресурсы доступными для сторонних сайтов, использующих конечную точку JSONP (GetSchedule). Если в запросе содержится куки, с помощью которого правильно идентифицирует пользователя, то для того, чтобы ограничить доступ к ресурсам, CalendarServer посылает ответ только данными этого пользователя.

Обратите внимание, что поскольку сервер использует конечную точку HTTP в качестве сервиса JSONP, любой сайт может к нему сделать запрос JSONP, в том числе вредоносные сайты. В данном сценарии страница рекламного баннера, полученная из CalendarServer, содержит тег script, с помощью которого делается запрос GetSchedule с функцией обратного вызова в качестве обертки padding, которая называется Leak (утечка). Обычно разработчик баннера AdBanner не имеет прямого доступа к сессионным куки атакуемого пользователя (MyCookie) на сервере CalendarServer. Тем не менее, поскольку запрос JSONP направляется на CalendarServer. браузер автоматически добавляется MyCookie как часть запроса; CalendarServer, получив запрос JSONP с куки MyCookie, вернется ресурс атакуемого пользователя (MySchedule), обернутый внутри Leak (рис.17.12.).

Рис.17.12. Контрпример JSONP в момент времени 0

На следующем шаге браузер интерпретирует ответ JSONP как вызов Leak(MySchedule) (рис.17.13). Остальная часть атаки проста; Leak может просто быть запрограммирован для передачи входного аргумента на сервер EvilServer, что позволяет злоумышленнику получить доступ к конфиденциальной информации жертвы.

Рис.17.13. Контрпример JSONP в момент времени 1

Эта атака, пример подделки межсайтового запроса - cross-site request forgery (CSRF), показывает слабость метода JSOPN; любой сайт в интернете может сделать запрос JSONP просто путем добавления тега <script> и получить доступ к данным, находящимся внутри обертки. Риск можно уменьшить двумя способами: 1) убедиться в том, что запрос JSONP никогда не возвращает конфиденциальные данные, или 2) использовать другой механизм вместо куки (например, секретные токетны), с помощью которых санкционируется запрос.

Метод PostMessage

Метод PostMessage является новой функцией языка HTML5, которая позволяет скриптам из двух документов (из, возможно, скачанных из различных мест) взаимодействовать друг с другом. Он предлагает более дисциплинированную альтернативу методу настройки свойства domain, однако добавляет свои собственные риски безопасности.

PostMessage является функцией API браузера, у которой два аргумента: 1) данные, которые должны быть посланы (message), и 2) место, указывающее откуда документы были получены (targetOrigin):

sig PostMessage extends BrowserOp {
  message: Resource,
  targetOrigin: Origin
}

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

sig ReceiveMessage extends EventHandler {
  data: Resource,
  srcOrigin: Origin
}{
  causedBy in PostMessage
  -- Событие "ReceiveMessage" посылается в скрипт с правильным контекстом
  origin[to.context.src] = causedBy.targetOrigin
  -- соответствие сообщений
  data = causedBy.@message
  -- источник, откуда берется скрипт, берется как параметр "srcOrigin" 
  srcOrigin = origin[causedBy.@from.context.src]
}

Браузер передает два параметра в ReceiveMessage: ресурс (data), который соответствует посылаемому сообщению, и параметр, указывающий происхождение, откуда документ был послан (srcOrigin). Сигнатура fact содержит четыре ограничения с тем, чтобы гарантировать, что каждое сообщение ReceiveMessage будет хорошо сформированным относительно соответствующему ему сообщению PostMessage.

Анализ: Опять же, давайте спросим анализатор языка Alloy, является ли метод PostMessage безопасным способом выполнения кросс-доменного взаимодействия. На этот раз, анализатор возвращает контрпример для свойства целостности Integrity, то есть атакующий может использовать слабость метода PostMessage для того, чтобы добавить вредоносные данные в доверенное приложение.

Обратите внимание, что по умолчанию в механизме PostMessage нет ограничения, указывающее кому разрешено отправлять сообщение PostMessage; другими словами, любой документ может отправить сообщение другому документу, если в последнем есть зарегистрированный обработчик сообщений ReceiveMessage. Например, в следующем экземпляре системы, сгенерированном на языке Alloy, скрипт EvilScript, работающий внутри AdBanner, посылает вредоносное сообщение PostMessage в документ, который был получен из домена EmailDomain (рис.17.14.).

Рис.17.14. Контрпример PostMessage в момент времени 0

Затем браузер затем пересылает это сообщение в документ (документы) с учетом того, откуда они были получены (в данном случае, из InboxPage). Если специально не проверять InboxScript и не отфильтровывать сообщения, идущих из нежелательных источников, то страница InboxPage примет вредоносные данные, что может привести к дальнейшим атакам безопасности. Например, можно вставлять кусок кода на языке JavaScript, который выполнит атаку XSS. Это показано на рис.17.14.

Рис.17.15. Контрпример PostMessage в момент времени 1

Как показывает этот пример, метод PostMessage не является по умолчанию безопасным, а чтобы обеспечить безопасность принимающего документа, требуется дополнительно проверять параметр srcOrigin с ,тем чтобы гарантировать, что сообщение исходит от авторитетного документа. К сожалению, на практике, многие сайты пропускают эту проверку, что позволяет вредоносный документам внедрять плохое содержание в виде фрагмента сообщения PostMessage [1].

Однако отсутствие проверки источника, откуда поступил документ, может быть не просто результатом программистского незнания. Реализация соответствующей проверки входящего сообщения PostMessage может оказаться сложной; в некоторых приложениях трудно заранее определить список доверенных источников, откуда, как ожидается, будут получены сообщения. (В некоторых приложениях этот список может даже меняться динамически). Это, опять же, подчеркивает противоречие между безопасностью и функциональностью: метод PostMessage можно использовать для безопасного кросс-доменного взаимодействия, но только тогда, когда известен список всех доверенных источников, откуда поступают документы.

Кросс-доменное использование ресурсов

Кросс-доменное использование ресурсов (Cross-Origin Resource Sharing — CORS) представляет собой механизм, который позволяет серверу обмениваться своими ресурсами с сайтами, полученных из различных источников. В частности, механизм кросс-доменного использования ресурсов может использовать скрипт из одного источника, чтобы сделать запросы на сервер, представляющий собой другой источник, эффективно обходя кросс-доменные ограничения при выполнении кросс-доменных запросов Ajax.

Вкратце, типичный процесс кросс-доменное использование ресурсов состоит из двух шагов: 1) скрипт, желая получить доступ к ресурсу на чужом сервере, имеет в своем запросе заголовок "Origin", в котором указывается происхождение скрипта, и 2) сервер добавляет заголовок "Access-Control-Allow-Origin» как часть своего ответа, который указывает на набор источников, откуда разрешен доступ к ресурсам сервера. Как правило, без кросс-доменное использование ресурсов браузер будет, прежде всего, в соответствие с кросс-доменные ограничениями, препятствовать скрипту создавать кросс-доменный запрос. Тем не менее, при включенном механизме кросс-доменного использования ресурсов браузер позволяет скрипту отправить запрос и получить доступ к его ответу, но только тогда, когда значение, указанное в "Origin", является одним из тех, что указаны в заголовке "Access-Control-Allow-Origin".

Кросс-доменное использование ресурсов дополнительно включает в себя понятие предварительных (preflight) запросов, которое здесь не рассматривается и предназначено для поддержки сложных кросс-доменных запросов, выходящих за рамки запросов GET и POST.

В языке Alloy, мы моделируем запрос на кросс-доменное использование в виде запроса XmlHttpRequest специального вида с двумя дополнительными полями origin и allowedOrigins:

sig CorsRequest in XmlHttpRequest {
  -- заголовок "origin" в запросе от клиента
  origin: Origin,
  -- заголовок "access-control-allow-origin" в ответе от сервера
  allowedOrigins: set Origin
}{
  from in Script
}

Затем мы используем fact corsRule языка Alloy для описания того, что представляет собой правильный запрос на кросс-доменное использование ресурсов:

fact corsRule {
  all r: CorsRequest |
    -- исходный заголовок запроса CORS соотвествует контексту скрипта
    r.origin = origin[r.from.context.src] and
    -- указанный источник является одним из допустимых источников
    r.origin in r.allowedOrigins
}

Анализ: Может ли неправильное кросс-доменное использование ресурсов позволить атакующему поставить под угрозу безопасность доверенного сайта? В ответ на запрос, анализатор языка Alloy возвращает простой контрпример для свойства конфиденциальности Confidentiality.

Здесь, разработчик приложения calendar решает поделиться некоторыми из своих ресурсов с другими приложениями с помощью механизма кросс-доменного использование ресурсов. К сожалению, CalendarServer сконфигурирован так, что в заголовках access-control-allow-origin ответов, используемых в механизме кросс-доменного использования ресурсов, возвращает значение из Origin (которое представляет собой множество всех источников, откуда можно получать ресурсы). В результате этого скрипту любого происхождения, в том числе и из домена EvilDomain, разрешается делать межсайтовый запрос на сервер CalendarServer и получать от него ответ (рис.17.16).

Рис.17.16. Контрпример кросс-доменного использования ресурсов

В этом примере подчеркивается одна общая ошибка, которую делают разработчики при кросс-доменном использовании ресурсов: Использование универсального значения "*" в качестве значения, указываемого в заголовке "access-control-allow-origin", позволяет любому сайту получать на сервере доступ к ресурсу. Эта модель доступа подходит для случая, когда ресурс считается общим и доступным для всех. Однако оказывается, что на многих сайтах значение "*" используется в качестве значения, указываемого по умолчанию, даже для частных ресурсов, непреднамеренно позволяя вредоносным скриптам получать доступ к этим ресурсам через запросы механизма кросс-доменного использования ресурсов [2].

Почему же разработчик использовал универсальное значение? Оказывается, что выяснение какой из источников происхождения будет допустимым может оказаться сложным, поскольку в момент разработки может быть неясно, какому из источников на этапе выполнения приложения следует предоставить доступ (аналогично проблеме PostMessage, которая рассматривалась выше). Сервис может, например, разрешать сторонним приложениям подписываться на свои ресурсы динамически.

В этой главе мы сконструировали документ, который помогает получить четкое представление о кросс-доменных ограничениях и связанных с ними механизмов построения модели политики на языке, который называется Alloy. Наша модель кросс-доменных ограничений не является реализация в традиционном смысле, и ее нельзя использовать в отличие от примерах, о которых рассказано в других разделах. Вместо этого мы хотели продемонстрировать ключевые элементы нашего подхода к "моделированию в стиле agile": 1) начали с небольшой, абстрактной модели системы и постепенно добавляя детали по мере их необходимости, 2) определили свойства, которым система, как предполагается, должна удовлетворять и (3) применили анализ для того, чтобы изучить потенциальные недостатки в проектировании системы. Конечно, эта глава была написана вскоре после того, как были впервые введены кросс-доменные ограничения, но мы считаем, что этот вид моделирования может быть потенциально еще более полезным, если его использовать на ранних стадиях проектирования системы.

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

Перейти к следующей части статьи.

Перейти к началу статьи.