Перейти к содержанию

Граница origin и CORS

Редакторский интерфейс может жить на https://editor.catalog.example, а API — на https://catalog.example. Пользователь тот же, но origin различается. Браузер применяет правила межorigin доступа, чтобы код одного источника не получил произвольный ответ другого. CORS позволяет серверу объявить конкретное разрешение в этой браузерной модели.

В этом уроке разрешим доверенный editor и проследим PUT с cookie. Результат — вы различаете предварительную проверку и фактическое изменение, а также понимаете, почему правильный CORS не доказывает авторизацию. Запросы ниже не отправлялись: это смысловые схемы будущей работы браузера и сервиса.

Origin не совпадает с site

Origin состоит из схемы, узла и порта. Два указанных HTTPS-узла имеют разные origin даже при общей части имени. При этом они относятся к одному schemeful site. Следовательно, SameSite=Lax не превращает запрос автоматически в same-origin и не отменяет CORS.

Это особенно важно для cookie. Host-only сессия относится к catalog.example, но страница editor может попросить браузер обратиться именно к этому узлу. Для такого fetch она явно выбирает credentials include. Подходящая cookie может отправиться при остальных условиях; браузерные ограничения стороннего хранения тоже остаются применимыми.

const response = await fetch('https://catalog.example/api/courses/c001', {
  method: 'PUT',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'If-Match': '"c001-json-r1"',
    'X-CSRF-Token': 'csrf-alice'
  },
  body: JSON.stringify({title:'JavaScript для веба',topic:'frontend',lessons:40})
});

Это полный самостоятельный фрагмент обращения, но credentials и CSRF-токен условны. Cookie не устанавливается вручную этим кодом. Origin тоже добавляет браузер по своему контексту. Сам фрагмент не запускался и не доказывает, что API уже доступен.

Предварительная проверка

PUT и используемые поля требуют preflight в рассматриваемом CORS-сценарии. Браузер сначала спрашивает, допускает ли сервер метод и поля для данного origin. Смысловой запрос:

OPTIONS /api/courses/c001 HTTP/1.1
Host: catalog.example
Origin: https://editor.catalog.example
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type,if-match,x-csrf-token

Preflight не предъявляет сессию как обычный фактический запрос. Поэтому требование войти для самого OPTIONS может сорвать допустимое обращение до отправки PUT. Обработчик проверяет выбранную CORS-политику, а не применяет изменение курса. Права Alice будут проверены независимо на следующем этапе.

Наш сервер разрешает точный origin editor, нужные методы и объявленные поля. Ожидаемый ответ:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://editor.catalog.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, If-Match, Idempotency-Key, Authorization
Access-Control-Max-Age: 300
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers

204 не содержит тела. Max-Age относится к сохранению разрешения предварительной проверки в браузерном механизме, а не к свежести курса на пять минут. Содержимое и условный ETag по-прежнему имеют собственные правила. Разрешённый метод CORS также не гарантирует, что данный путь поддерживает его: реальный маршрутизатор способен вернуть 405.

Фактическая операция

После разрешённого preflight браузер посылает PUT с Origin, выбранной cookie, CSRF-токеном и If-Match. Теперь сервер устанавливает личность, проверяет frontend-права Alice, Origin и токен, входной документ и текущую версию. Предварительный ответ не заменяет ни одну из этих границ.

Сам ответ PUT должен иметь разрешающие CORS-поля для доверенного origin, включая Access-Control-Allow-Credentials true. Без них браузерная программа может не получить доступ к результату, даже если изменение уже было выполнено. Такое состояние требует честной обработки неопределённости: ошибка чтения CORS не доказывает отмену серверной записи.

Когда нужные поля выбираются по Origin, сохраняется Vary: Origin, а для варианта курса также Vary: Accept. Не надо перезаписывать прежнее значение Vary при добавлении CORS и терять учёт формата. Список зависимостей должен отражать оба решения.

Для чтения ETag, Location или Retry-After программой разрешаем их через Access-Control-Expose-Headers. Это отличается от Allow-Headers, которое относится к полям запроса. Два похожих имени управляют разными сторонами обмена. Без expose клиент может видеть успешное тело, но не нужную ему метку для следующего PUT.

Точный origin вместо звёздочки

В credentialed-договоре нельзя использовать Access-Control-Allow-Origin со звёздочкой вместо конкретного origin. Наш сервер сопоставляет строку с закрытым перечнем и возвращает точное разрешённое значение. Он не отражает любой присланный Origin автоматически.

Проверка «имя содержит catalog.example» тоже неверна: постороннее имя может включить такую подстроку. Политика определяет полноценную схему, узел и порт, без случайного допуска по текстовому сходству. Значение null не является доверенным editor и в нашем договоре отвергается.

CORS не является общим сетевым файрволом. Обычный программный клиент вне браузера может отправить запрос без соблюдения этой политики. Сервер всё равно проверяет credentials и разрешения. Если закрыть данные только отсутствием CORS-полей, они останутся доступными тому, кто обращается другим инструментом.

Ошибка операции тоже нуждается в доступном ответе

Пусть preflight прошёл, но PUT получил 412 из-за устаревшего If-Match. Для доверенного editor этот problem также должен сопровождаться нужными CORS-полями. Иначе JavaScript увидит только невозможность прочитать ответ и не сможет предложить обновить форму. При этом сервер не обязан делать отказ успешным: разрешение чтения и бизнес-статус независимы.

Та же логика действует при 401 и 403. Клиенту полезно узнать, требуется ли вход, не хватает ли права или устарела версия. Произвольное отключение CORS на ошибках стирает различие и провоцирует бесполезные повторы. Сервер должен формировать политические поля согласованно в общей обработке допустимого origin, не раскрывая закрытые данные в самом problem.

Для cookie-варианта браузер ещё применяет правила выбора сессии. Allow-Credentials:true не создаёт cookie и не заставляет принять Set-Cookie вопреки независимой политике. Поэтому успешный OPTIONS и правильные разрешающие поля сами по себе не доказывают действующий вход. Наличие Cookie в фактическом запросе и действительность записи оцениваются отдельно.

Если политика разрешённых заголовков или методов меняется, ранее сохранённый preflight может некоторое время влиять на следующий сценарий. Max-Age выбирают с учётом такой задержки, а фактическое разрешение операции проверяют каждый раз. Снятие редакторского права Alice не должно ждать истечения браузерного разрешения CORS: последнее говорит о технической возможности обмена между origin, а не о роли пользователя.

Все эти ветки сейчас описаны, но не наблюдались в браузере. Будущий опыт должен сравнивать отказ самого сервера и доступность его ответа, сохраняя обе причины, вместо единственной записи «запрос не удался».

Простое обращение способно уйти

Для некоторых запросов браузеру не нужен preflight. Поэтому запрет читать ответ не всегда означает, что запрос вообще не отправлен. Особенно опасно считать CORS единственной защитой изменения, если сервис принимает простой POST с автоматически выбранной cookie.

Наш API требует CSRF-условия для cookie-записей независимо от предварительной проверки. Доверенный origin проходит обе политики, недоверенный не получает право изменения. Следующий урок подробно объяснит эту связь, сохранив различие чтения ответа и намерения пользователя.

При ошибке клиента нужно узнать, на каком этапе остановился сценарий: preflight, фактическая авторизация, условие версии или доступ JavaScript к ответу. Один общий текст «CORS не работает» скрывает эти состояния и способен привести к опасному расширению allowlist ради исчезновения сообщения.

Стандарт Fetch о CORS описывает механизм браузера. Наш договор дополнительно определяет один доверенный origin, cookies и предметные права. Реальное совместимое поведение будущего сервера и браузера ещё не проверено. Теперь межorigin чтение понятно; дальше отдельно защитим изменения, которым браузер способен автоматически придать действующее основание сессии.

Примеры продолжения серии. Оглавление курса.