[Перевод] CORS — это тупо
Технология CORS и действующее в браузерах правило ограничения домена — те вещи, которые часто понимаются превратно. Ниже я объясню, что они собой представляют, и почему пора перестать волноваться по их поводу.
Замечание: я собираюсь рассказать о CORS и правиле ограничения домена как о единой сущности, поэтому далее часто буду употреблять эти термины как синонимы. Дело в том, что они, по сути — части одной системы, работают в сочетании друг с другом и помогают вам решать, что можно сделать с какими ресурсами смешанного происхождения. В принципе, если ваши запросы поступают из разных источников, то вам придётся иметь дело с правилами, политиками и механизмами CORS.
Прежде всего, отмечу, что CORS — это огромный костыль, помогающий снизить влияние ошибок, передающихся с унаследованным кодом. В этой системе защита предоставляется как по принципу отказа от участия (opt-out) в попытке частично купировать XSRF-атаки против незащищённых или немодифицированных сайтов, так и по принципу активного участия (opt-in), чтобы на сайте включалась активная самозащита. Но ни одной из этих мер не достаточно, чтобы решить целенаправленно созданную проблему. Если на вашем сайте используются куки, то вы обязаны деятельно позаботиться о его безопасности. (Ладно, это касается не любого сайта, но лучше перестрахуйтесь. Выделите время на тщательный аудит вашего сайта или выполните описанные ниже простые шаги. Даже придерживаясь самых разумных паттернов, вы всё равно можете подставиться под XSRF-уязвимости).
Проблема
Ключевая проблема здесь заключается в том, как именно в Вебе обрабатываются неявные учётные данные. Ранее браузеры решали эту проблему катастрофическим образом: считалось, что такие данные можно включать в междоменные запросы. В результате открывался такой вектор атаки.
1. Залогиниться в https://your-bank.example.
2. Перейти в https://fun-games.example.
3. Тогда https://fun-games.example
выполнит fetch("https://your-bank.example/profile")
, что выдаст злоумышленнику конфиденциальную информацию о вас — например, ваш адрес или баланс вашего счёта. Этот метод срабатывал, поскольку, когда вы заходите на сайт банка, банк выдаёт вам куки, через который можно получить доступ к деталям вашего счёта. Тогда как un-games.example
не может просто украсть этот куки, он может направлять собственные запросы к API вашего банка, и браузер с готовностью прикрепит этот куки, чтобы вас можно было аутентифицировать.
Решение
Тут в дело и вступает CORS. Эта технология регламентирует, как именно можно выполнять и использовать междоменные запросы. Данная технология очень гибкая и при этом совершенно неполноценная.
По умолчанию в соответствии с такой политикой можно делать запросы, но нельзя читать результаты. Поэтому fun-games.example не может прочитать ваш адрес из https://your-bank.example/profile
. Кроме того, можно добыть информацию окольными путями — например, через задержку, или узнать подробности, проверив, успешно или неуспешно выполнен запрос.
Но эта технология только всех раздражает, а саму проблему не решает! Да, fun-games.example не может прочитать результат, но запрос всё равно отправляется. Соответственно, скрипт может выполнить POST https://your-bank.example/transfer?to=fungames&amount=1000000000
и переправить миллиард долларов на счёт своего хозяина.
Вероятно, это одна из серьёзнейших брешей в безопасности, допущенных во имя обратной совместимости. Суть в том, что автоматически предоставляемые механизмы междоменной защиты на практике совершенно не работают. Абсолютно на всех сайтах, использующих куки, взаимодействия с куки должны обрабатываться явно.
Да, на всех до единого.
Как на самом деле решается такая проблема
Ключевое средство защиты от таких межсайтовых атак — гарантировать, что неявно передаваемые учётные данные не будут использоваться ненадлежащим образом. Для начала лучше просто игнорировать все такие данные при межсайтовых запросов, а затем добавлять в эту политику конкретные исключения по мере необходимости.
Внимание: Не существует такой комбинации заголовков Access-Control-Allow-*
, которая решала бы проблему с простыми запросами. Они выполняются задолго до того, как будет проверена какая-либо политика. Вам придётся обрабатывать их иначе. Не пытайтесь исправить ситуацию с ними, устанавливая CORS-политику.
Наилучшее решение — настроить на стороне сервера промежуточное ПО таким образом, чтобы оно игнорировало неявные учётные данные при всех межсайтовых запросов. В следующем примере отсекаются куки, но, если вы используете HTTP-аутентификацию или клиентские TLS-сертификаты, то тоже обязательно игнорируйте эти данные. К счастью, во всех современных браузерах уже доступны заголовки Sec-Fetch-* . С их помощью межсайтовые запросы легко идентифицируются.
def no_cross_origin_cookies(req):
if req.headers["sec-fetch-site"] == "same-origin":
# Одинаковый источник, OK
return
if req.headers["sec-fetch-mode"] == "navigate" and req.method == "GET":
# GET-запросы не должны изменять состояние, так что это безопасно.
return
req.headers.delete("cookie")
Это и есть надёжная базовая защита. Если потребуется, то можно добавить конкретные исключения для тех конечных точек, которые специально подготовлены для обращения с неявно аутентифицируемыми междоменными запросами. Категорически не рекомендую пользоваться широкими исключениями.
Подробно о защите
Явные учётные данные
Один из лучших способов в принципе избежать такой проблемы — это отказаться от использования неявных учётных данных. Если вся аутентификация проводится через явные учётные данные, то не придётся беспокоиться и о том, что браузер добавит какие-то неожиданные куки. Явные учётные данные можно получить, подписавшись на токен API или через поток OAuth. Но в любом случае здесь наиболее важно следующее: если войти на один сайт под некоторыми учётными данными, то другие сайты не смогут ими воспользоваться. Лучше всего в таком случае передать токен аутентификации в заголовке Authorization
.
Authorization: Bearer chiik5TieeDoh0af
Задействовать заголовок Authorization
— это стандартное поведение, и оно хорошо поддерживается многими инструментами. Например, этот заголовок, скорее всего, будет по умолчанию удаляться из большинства логов.
Но наиболее важно, что его должны явно устанавливать все клиенты. Так не только решается проблема с XSRF, но и чрезвычайно упрощается поддержка множественных аккаунтов.
Основной недостаток такой технологии в том, что явные учётные данные неприменимы при работе с сайтами, использующими серверный рендеринг, так как они не включаются в высокоуровневую навигацию. С другой стороны, серверный рендеринг сильно повышает производительность, поэтому такая техника зачастую не подходит.
SameSite-куки
Даже притом, что наш сервер должен игнорировать куки при междоменных запросах, рекомендуется вообще по возможности не включать их в запросы. Следует установить для всех ваших куки атрибут SameSite=Lax
, и тогда браузер станет опускать их при междоменных запросах.
Примечание: Говоря о «высокоуровневой» навигации я имею в виду тот URL, что фигурирует в адресной строке браузера. Соответственно, если вы загрузите через эту строку fun-games.example
, и браузер выполнит запрос к your-bank.example
то fun-games.example
— это сайт верхнего уровня.
Важно помнить, что куки по-прежнему включаются в навигационную информацию при GET-переходах верхнего уровня. Чтобы этого избежать, можно использовать SameSite=Strict
, но в таком случае будет казаться, что пользователь разлогинился на первой странице после того, как перешёл по междоменной ссылке (поскольку в этом запросе не будет куки).
Если использовать куки SameSite
, то также не получится организовать межсайтовое заполнение форм, причём, от этого невозможно избирательно отказаться для нескольких конкретных конечных точек. К счастью, на практике такой случай встречается очень редко, и вполне можно его просто не предусматривать. Определённо рекомендую устанавливать этот атрибут в значение по умолчанию и прибегать к другим механизмам лишь в тех случаях, когда это явно требуется.
CORS-политика
Вот простая политика, которую можно скопировать и вставить:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Всё, дело сделано.
Внимание: эта политика не просто отзеркаливает заголовок Origin
в заголовке Access-Control-Allow-Origin
. То есть, * не просто играет роль джокера, но и отключает неявные учётные данные. Так гарантируется, что невозможно будет проводить аутентифицированные запросы из других источников, кроме как в рамках явного потока событий, например, с включением заголовка Authorization
.
В результате данной политики другие сайты могут выполнять только анонимные запросы. То есть, безопасность будет на том же уровне, как если бы вы делали такие запросы через CORS-прокси.
Не нужно ли больше конкретики?
Пожалуй, нет. На то есть пара причин:
Может создаться ложное чувство безопасности. Если другая веб-страница просто открывается в «корректно функционирующем» браузере и, конечно, не может выполнять таких вредоносных запросов, это ещё не означает, что такие запросы невозможны в принципе. Например, очень распространены CORS-прокси.
В таком случае к вашему сайту закрывается доступ «только для чтения», который мог бы пригодиться для предпросмотра URL, выборки новостных лент или реализации других возможностей. В результате используется всё больше CORS-прокси, что плохо сказывается не только на производительности, но и на пользовательской приватности.
Помните, CORS — не для блокирования доступа, а для того, чтобы нельзя было случайно переиспользовать неявные учётные данные.
Моё возмущение
Так для чего мне всё это знать, почему веб не является безопасным по умолчанию. Почему я вынужден иметь дело с неэффективной политикой, которая по умолчанию меня просто бесит, не решая никаких реальных проблем?
Да, я знаю, это весьма раздражает. Думаю, большинство из описанных проблем коренится в обратной совместимости. На сайтах реализованы фичи, написанные прямо поверх этих дыр в безопасности, и браузеры стараются латать эти дыры насколько возможно, не нарушая при этом работу уже существующих сайтов.
К счастью, на горизонте просматриваются признаки отрезвления — то есть, браузеры действительно готовы поломать некоторые сайты во благо пользователя. Крупные браузеры развиваются в сторону изоляции доменов верхнего уровня. Эта технология называется по-разному: в Firefox это State Partitioning (разделение состояния), в Safari — Tracking Prevention (предотвращение отслеживания), а в Google предпочитают термин «cross-site tracking cookies» (куки для отслеживания межсайтовых взаимодействий). Фактически, здесь реализована CHIPS-система, на использование которой требуется активное согласие.
Главная проблема в том, что эти подходы реализуются в плоскости обеспечения приватности, а не безопасности. Таким образом, полагаться на них нельзя, поскольку применяемая в них эвристика иногда допускает междоменный обмен неявными учётными данными. CHIPS в данном отношении даже лучше, поскольку надёжно работает в поддерживающих её браузерах. Но эта система поддерживает только куки.
Поэтому складывается впечатление, что браузеры уходят от использования таких куки, которые захватывают и контексты верхнего уровня, но пока это нескоординированное шатание. Также неясно, какой механизм станет господствующим — блокирование сторонних куки (Safari) или разделение (Firefox, CHIPS).