Технология 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"), что выдаст злоумышленнику конфиденциальную информацию о вас — например, ваш адрес или баланс вашего счёта. Этот метод срабатывал, поскольку, когда вы заходите на сайт банка, банк выдаёт вам куки, через который можно получить доступ к деталям вашего счёта. Тогда как fun-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-прокси.

Не нужно ли больше конкретики?

Пожалуй, нет. На то есть пара причин:

  1. Может создаться ложное чувство безопасности. Если другая веб-страница просто открывается в «корректно функционирующем» браузере и, конечно, не может выполнять таких вредоносных запросов, это ещё не означает, что такие запросы невозможны в принципе. Например, очень распространены CORS-прокси.

  2. В таком случае к вашему сайту закрывается доступ «только для чтения», который мог бы пригодиться для предпросмотра URL, выборки новостных лент или реализации других возможностей. В результате используется всё больше CORS-прокси, что плохо сказывается не только на производительности, но и на пользовательской приватности.

Помните, CORS – не для блокирования доступа, а для того, чтобы нельзя было случайно переиспользовать неявные учётные данные.

Моё возмущение

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

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

К счастью, на горизонте просматриваются признаки отрезвления — то есть браузеры действительно готовы поломать некоторые сайты во благо пользователя. Крупные браузеры развиваются в сторону изоляции доменов верхнего уровня. Эта технология называется по‑разному: в Firefox это State Partitioning (разделение состояния), в Safari — Tracking Prevention (предотвращение отслеживания) а в Google предпочитают термин «cross‑site tracking cookies» (куки для отслеживания межсайтовых взаимодействий). Фактически, здесь реализована CHIPS-система, на использование которой требуется активное согласие.

Главная проблема в том, что эти подходы реализуются в плоскости обеспечения приватности, а не безопасности. Таким образом, полагаться на них нельзя, поскольку применяемая в них эвристика иногда допускает междоменный обмен неявными учётными данными. CHIPS в данном отношении даже лучше, поскольку надёжно работает в поддерживающих её браузерах. Но эта система поддерживает только куки.

Поэтому складывается впечатление, что браузеры уходят от использования таких куки, которые захватывают и контексты верхнего уровня, но пока это нескоординированное шатание. Также неясно, какой механизм станет господствующим: блокирование сторонних куки (Safari) или разделение (Firefox, CHIPS).

Комментарии (17)


  1. Mnemonik
    03.09.2024 09:58
    +31

    Чё-то я ваще не согласен. Статья какого-то страшно разгневанного нерда у которого интернет не работает так как ему нравится.

    "Соответственно, скрипт может выполнить POST https://your-bank.example/transfer?to=fungames&amount=1000000000 и переправить миллиард долларов на счёт своего хозяина." - нет не выполнит. любой браузер сначала отправит чистый OPTIONS вообще без всяких авторизаций, и если не получит в ответ CORS политику разрешающую и запрос и отправку куки - ни пса он не выполнит.

    "Один из лучших способов в принципе избежать такой проблемы – это отказаться от использования неявных учётных данных." - нифига мне не нравится такой вариант. в таком варианте токен авторизации будет где-то болтаться посреди javascript, где-то его можно будет прочитать из payload и так далее. тогда как cookie можно поставить httponly, и она будет в принципе недоступна javascript. авторизация будет разделена от функциональности и выковыривать её если что надо будет из другого места, если уж удалось заинжектить клиенту вредоносный скрипт.

    "Наилучшее решение — настроить на стороне сервера промежуточное ПО таким образом, чтобы оно игнорировало неявные учётные данные при всех межсайтовых запросов." - так можно было бы их вообще запретить в браузере и дело с концом! раз кому-то они так не нравятся. но дело-то в том что они как раз нужны в сложных системах. например есть веб морда какого-то сложного сервиса, который тянет API с нескольких микросервисов, не имеет своего состояния, и использует как раз запросы на микросервисы по другим URL. и вот тут-то всё это склеивается нормально настроеной CORS политикой. для чего она собственно и сделана и в контексте чего отлично работает и все её опции и методы работы понятны. независимо от того что некоторых операторов локалхоста эти все "ненужные" сложности бесят.


    1. DonVietnam
      03.09.2024 09:58

      О да, мне было лень все это расписывать, когда прочитал это статью, вернулся, чтобы проверить, нашлись ли добрые люди)


    1. breninsul
      03.09.2024 09:58
      +1

      ну видит JS токен авторизации. А минусы где?

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


      1. Kergan88
        03.09.2024 09:58

        >какого черта левый сайт может сделать запрос с сенсетив информацией

        Так не может же.


  1. fransua
    03.09.2024 09:58

    Спасибо за статью.
    Не могу понять, почему поголовно не делают фронтенд-прокси на том же домене что и фронтенд, работающий по strict-origin без CORS только для этого фронтенда. Можно не бояться xsrf, не беспокоиться о правильной настройке CORS (в вашем примере не хватает allow-headers, которые часто нужны). И options запросов нет, которые так мешают фронтендерам.
    А рядом если надо еще апишку для external клиентов с CORS но без cookie авторизации.


    1. Mnemonik
      03.09.2024 09:58
      +2

      Так делают... Это самое простое решение к которому склоняются все, когда встаёт выбор - разобраться с такой довольно сложной вещью как CORS, поставить Allow: *, сделать всё на том же хосте с прокси к сервисам. Чаще всего опыта чтобы отбросить уровень 0 (поставить Allow: *) уже хватает, а вот количество усилий нужное на качественный скачок до уровня 2 - сделать CORS по уму, по сравнению с уровнем 1 - сбацать всё на прокси кажется просто непропорциональным.

      И я честно даже не буду играть в сноба от того что я разобрался с CORS, можно и на прокси. Действительно реальных технических плюсов выделяющий тот или иной подход - нет. Кроме того что прокси понятней и проще.

      Тут кстати очень характерна недавняя история с Google Chrome который собрался депрекейтить cross-site cookie в ноль, так чтобы они вообще не работали в браузере. У них так и было написано в дисклеймере, если вам реально нужно передавать авторизацию на сторонние сайты - делайте прокси через тот сайт с которым вы работаете. То есть даже большие корпорации "делающие" наш интернет вполне адекватно оценивают сложности того или иного подхода. Но всё же дело закончилось тем, что они отменили своё решение, и cross-site cookie продолжат работу, потому что видимо всё же нашлись реальные большие кейсы когда такой подход - верный. И мне кажется чтобы переубедить гугл нужны веские аргументы что вот такой подход (в этом случае часть CORS, даже не весь, о похоронах CORS как жаждет автор статьи речь даже не шла) - необходимая технология.


      1. fransua
        03.09.2024 09:58

        Интересно, есть ли разница в производительности, по идее OPTIONS тоже не бесплатный


        1. Mnemonik
          03.09.2024 09:58

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

          да, есть какие-то накладные расходы, но в вебе разница в 50 миллисекунд это... ну камон... у меня эта статья сейчас открывалась примерно полторы секунды пока всё догрузила...


      1. TimsTims
        03.09.2024 09:58

        И мне кажется чтобы переубедить гугл нужны веские аргументы

        Фича не взлетела по очень простой причине - у кучи пользователей сломались их сайты, им начали говорить чтобы сносили гугл хром, ставили Firefox/edge. Доля браузера начала уменьшать - продакты стали переживать - фичу откатили.


  1. Vest
    03.09.2024 09:58
    +1

    Жду статью о пользе встраивания в iframe сторонних сайтов без явных на то разрешений.


  1. SWATOPLUS
    03.09.2024 09:58

    Если пофантазировать, то правильное решение отказаться от поддержки кук браузером и сайтами вообще (за одно похоронить куки-банеры), и всем перейти на bearer jwt auth.

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

    Явное лучше неявного.


    1. sdramare
      03.09.2024 09:58

      Как от xss будете токен защищать?


  1. bankir1980
    03.09.2024 09:58

    Натыкался тут на новости, что хром собирался внедрить проверку на уровни сетей, а ля 192.168.*.*, 10.*.*.*, и прочие внешние и внутренние. И вот если запрос (получается междоменный) в разные ранги сетей, то он не пропускается. Вроде только на 127.0.0.1 из всех сетей пускает. Кто то слышал об этой фиче? Есть по ней новости?


  1. Kergan88
    03.09.2024 09:58

    По умолчанию в соответствии с такой политикой можно делать запросы, но нельзя читать результаты.

    А ни как иначе same-origin policy и не должна работать. Это просто универсальный механизм, который запрещает доступ к ресурсу на домене Х со стороны скрипта с домена Y. И речь не только о фетч-зхапросах - невозможность прочитать куки с другого домена, заглянуть внутрь ифрейма с другого домена или даже посмотреть содержимое загруженной с другого домена картинки - это все то самое same-origin policy.


  1. cstrike
    03.09.2024 09:58

    Помните, CORS – не для блокирования доступа, а для того, чтобы нельзя было случайно переиспользовать неявные учётные данные.

    А я думал чтобы как раз таки было можно. Без CORS все запрещено, а при включении CORS - немножечко можно.


  1. kalyukdo
    03.09.2024 09:58

    Ну да, это же так сложно принять на бэке option запрос, проверить домен и отдать для него правила CORS, и затем повториить этуже валидаю для get/post etc запросов. Эти правила помогают при работе с неограниченным кол-вом поддоменов и в целом доменов, возьмите как пример констуркторы сайтов.


  1. Dywar
    03.09.2024 09:58

    Еще есть CSRF токен - https://stackoverflow.com/questions/5207160/what-is-a-csrf-token-what-is-its-importance-and-how-does-it-work, которому CORS помогает.