CORS и preflight request
CORS (Cross-Origin Resource Sharing - шаринг ресурса для кросс-источника) — это стандарт безопасности, который позволяет клиентам (браузерам) использовать ресурсы (URL) с других источников (доменов). Он был создан для улучшения SOP (same-origin policy - политика единого источника). Эту политику используют браузеры для ограничения доступа к ресурсам (URL), находящимся на других доменах (которыми они не управляют).
Примеры CORS-ошибок (их можно увидеть в консоли браузера):
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://example.com/
Access to fetch at 'https://example.com' from origin 'http://localhost:3000' has been blocked by CORS policy.
💡 Пример
Представьте, вы создаёте веб-сервис, где пользователи могут сохранять свои записи онлайн. Ваш сайт (например, myapp.com) отправляет данные на внешний сервер (например, noteapi.com). Для этого вы используете метод POST.
Пример JavaScript-кода, выполняющего отправку записи:
const apiUrl = "https://noteapi.com/api/notes";
const userToken = "токен_авторизации";
const payload = {
title: "Запись дня",
content: "Сегодня я узнал, как отправлять данные на другой сервер"
};
fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${userToken}`
},
body: JSON.stringify(payload)
});
Как бразуер сделат запроса?
-
Предварительный запрос (Preflight Request): Так как
Content-Type = application/json, прежде чем отправить основной POST-запрос, браузер автоматически сделает предварительный запрос методомOPTIONSна сервер noteapi.com. Это нужно, чтобы узнать, разрешён ли такой запрос. -
Заголовки в Preflight: Браузер отправляет заголовки вроде:
Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type, Authorization Origin: https://myapp.com
-
Ответ сервера на Preflight: Если сервер настроен правильно, он отвечает, что запрос с указанными заголовками и методом разрешён от источника https://myapp.com.
- Основной запрос: Только после положительного ответа на preflight-запрос, браузер отправляет основной POST-запрос.
⚠️ Cross-origin request
Простыми словами - это запрос на другой домен.
Например, когда мы вставляем у себя на сайте картинку - <img> тег, в атрибуте src которого указываем ссылку на картинку с другого сайта, браузер в итоге будет делать Cross-origin request. Но мы обычно не испытываем проблем с <img> тегом, потому что политика для этого тега специфическая.
Чтобы запрос считался cross-origin, могут отличаться не только домен, но также "схема" (например https) или "порт" (например 80).
Например: http://example.com и https://example.com - это разные источники, потому что первый использует схему "http", а второй "https". Также тут разные порты: для http — 80, а для https — 443. Таким образом, эти источники отличаются по двум параметрам: схема и порт.
Путь до конкретного ресурса значения не имеет. Например http://example.com/api/post/25 и http://example.com/some-page будут считаться единым источником.
Популярные теги, которые могут делать кросс-доменные запросы:
<img><script>- если исполняемый внутри скрипт делает запрос, он должен соблюдать CORS.<frame><video><audio><iframe>- может быть ограничен SOP. См. X-Frame-Options<link><form>
🔒 Что такое SOP (same-origin policy)?
SOP (same-origin policy) - это функция безопасности браузера, которая ограничивает доступ к ресурсам веб-приложений. Требует, чтобы ресурс был из того же источника, что и веб-приложение, которое обращается к ресурсу.
SOP введена в Netscape Navigator 2.02 в 1995 году.
Все современные браузеры следуют этой политике в той или иной степени. Принципы описаны в RFC6454.
Для того чтобы два источника считались одинаковыми, они должны иметь одинаковые хост, схему и порт. Рассмотрим, например, URL https://blog.postman.com:3000:
- хост - blog.postman.com
- схема - https
- порт - 3000 - обычно исключается и равен 80 (http) или 443 (https).
SOP более гибкая для следующих типов ресурсов:
-
Изображения: Веб-страницы могут встраивать изображения из любого источника без ограничений.
-
CSS-стили: Внешние CSS могут быть загружены на веб-страницу с помощью тега
<link>непосредственно в HTML или правила@importв другом файле таблицы стилей. -
Scripts: Файлы JavaScript могут быть загружены и выполнены из любого источника, но они могут обращаться и изменять элементы DOM только из источника, в котором они загружены. Они подчиняются ограничениям SOP и не могут делать кросс-оригинные вызовы внешних API.
-
Медиа-файлы: Аудио-файлы и видео-файлы могут быть встроены и воспроизведены из различных источников с использованием тегов
<audio>или<video>. - iframe: Встроенные
<iframe>могут загружать контент из любого источника, но SOP изолирует контент внутри iframe от контента родительской страницы. iframe может общаться с родительской страницей, используя интерфейс, предоставленный JavaScript, но у него нет прямого доступа к ресурсам родительской страницы, потому что они не находятся в одном источнике.
🔓 Что такое CORS (cross-origin resource sharing)?
CORS (Cross-Origin Resource Sharing) — это механизм, позволяющий серверу разрешать доступ к своим ресурсам с других доменов через специальные HTTP-заголовки, обходя ограничения SOP.
Когда делается кросс-доменный запрос, браузер автоматически применяет CORS политику. Если сервер на на который делается запрос не разрешает использовать ресурс, браузер его блокирует.
SOP (Same-Origin Policy) по умолчанию ограничивает доступ к ресурсам между разными источниками на стороне клиента. CORS (Cross-Origin Resource Sharing) — позволяет открыть ресурс явно указав, с помощью HTTP-заголовков, кто может обращаться к их ресурсам. Таким образом, CORS позволяет обойти ограничения, налагаемые SOP.
‼️ Preflight requests (предварительный запрос)
Preflight запрос – это специальный запрос, который отправляется перед основным запросом к серверу. Он использует метод OPTIONS и содержит заголовки, которые информируют сервер о типе предстоящего запроса.
Если preflight request провалился браузер не будет делать основной запрос и вместо этого выведет CORS-ошибку.
Важные заголовки preflight запроса и ответа сервера
Заголовки в preflight запросе:
-
Access-Control-Request-Method: сообщает серверу, какой HTTP метод будет использоваться в основном запросе. -
Access-Control-Request-Headers: перечисляет заголовки, которые будут использоваться в основном запросе. Origin: указывает домен, с которого будет сделан запрос в основном запросе.
Заголовки в ответе на preflight запрос:
-
Access-Control-Allow-Origin: указывает, какие домены могут получать ответы. -
Access-Control-Allow-Methods: перечисляет методы, разрешенные для кросс-доменных запросов. -
Access-Control-Allow-Headers: указывает, какие заголовки разрешены в запросе. Access-Control-Max-Age: сообщает, как долго результат preflight запроса может быть кэширован.
В каких случаях делается Prelight запрос?
Браузер отправляет "preflight request" на сервер для любого действия, изменяющего данные. Это нужно, потому что запросы, обновляющие данные, не считаются безопасными, поэтому браузер сначала убеждается, что сервер разрешает CORS, прежде чем сделать сам запрос.
Preflight запрос делается, если выполняется любое из этих условий:
-
Метод не "простой":
Не GET, HEAD, POST. А например:DELETEиPUT. -
POST с "непростым" Content-Type:
Любой, кроме:- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
-
Есть нестандартные заголовки:
Например:- Authorization
- X-Requested-With
- Content-Type: application/json
- Любой X-*
-
Ответ требует credentials (withCredentials: true в JS)
И сервер не указывает Access-Control-Allow-Credentials: true. -
Запрос содержит custom headers или методы, не входящие в стандарт.
- Запрос использует fetch() с опциями, влияющими на CORS
Например, mode: 'cors' + нестандартный заголовок или метод.
Кэширование preflight ответов
Кэширование preflight ответов может улучшить производительность. Если сервер указывает в ответе на preflight запрос заголовок Access-Control-Max-Age, браузер может кэшировать разрешение на выполнение запросов на указанный период. Так, для повторных запросов к тому же серверу preflight запрос делаться не будет.
❗️ Simple requests (основной запрос)
Это CORS-запросы, которые отправляются в рамках основного HTTP-запроса. Чтобы простой запрос был отправлен на сервер, должны быть выполнены следующие критерии:
-
Запрос должен использовать один из следующих HTTP-методов:
GET,HEADилиPOST. -
Заголовки запроса должны быть базовыми, которые обычно автоматически устанавливаются клиентом. К простым заголовкам относятся:
AcceptAccept-LanguageContent-LanguageContent-Type
- Если запрос использует метод
POST, то заголовокContent-Typeдолжен иметь одно из значений:application/x-www-form-urlencodedmultipart/form-datatext/plain
🟢 Заголовки ответа CORS
Сервер использует заголовки ответа для предоставления информации о CORS браузеру. Ответ на CORS-запрос обычно содержит следующие заголовки:
-
Access-Control-Allow-Origin- сообщает браузеру, какие источники могут получить доступ к ресурсу. Он может содержать название самого источника (например,https://postman.com), или можно использовать символ подстановки -*. -
Access-Control-Allow-Credentials- является булевым значением, которое указывает, должен ли браузер добавлять учетные данные (файлы cookie или учетные данные аутентификации HTTP) при выполнении cross-origin запроса.- Если
falseустановлено в ответе на preflight request, то учетные данные не должны включаться в фактический запрос. - Если простой запрос сделан с этим заголовком, установленным в false, то браузер не будет включать эти данные в основной запрос.
- Если
-
Access-Control-Allow-Methods- используется в ответе на preflight request для указания разрешённых методов запроса в основном запросе. Пример:GET, POST, OPTIONS
-
Access-Control-Allow-Headers- используется в ответе на preflight request для указания разрешённых заголовков в основном запросе. Пример:DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range
-
Access-Control-Expose-Headers- указывает, какие заголовки могут быть доступны для браузера. Любой заголовок, не указанный здесь, не будет доступен для кода JavaScript на стороне клиента. Пример:Content-Length,Content-Range
Access-Control-Max-Age- указывает максимальное время (в секундах), в течение которого ответ на preflight request может быть кэширован.
Успешный ответ на CORS-запрос может включать любой код состояния HTTP, при условии, что он содержит один или несколько из перечисленных выше заголовков ответа. Однако успешный ответ на запрос предварительной проверки CORS должен указывать статус OK (т.е. 200 или 204).
🛡 Преимущества работы с CORS
CORS повышает общую безопасность приложения, улучшая гибкость и взаимодействие. Основные преимущества:
-
Улучшенная безопасность API: CORS защищает от несанкционированных запросов к чувствительным ресурсам на другом домене. Расслабляя политику одного источника (SOP) и требуя явного разрешения через заголовки CORS, он уменьшает риск атак подделки межсайтовых запросов (CSRF - cross-site request forgery) и несанкционированного доступа к данным.
-
Cross-origin аутентификация: CORS обеспечивает безопасную аутентификацию путём контроля над передачей учетных данных (таких как файлы cookie или токены аутентификации) в кросс-доменных запросах. Это важно при использовании технологии единого входа (SSO - Single Sign-On) и других механизмов аутентификации.
-
Стандартизация: CORS - это стандартизированный механизм, поддерживаемый всеми основными веб-браузерами. Эта единая реализация во всех браузерах обеспечивает последовательный и надёжный подход к обработке кросс-доменных запросов.
- Интеграция API: CORS необходим для интеграции веб-приложений с внешними API, позволяя разработчикам использовать функциональность сторонних сервисов в своих собственных сервисах.
👍 Лучшие практики для реализации CORS
Хотя CORS был разработан для безопасного изменения same-origin политики, он всё же должен быть правильно реализован. Неправильная реализация CORS может ухудшить безопасность данных. Поэтому важно следовать лучшим практикам отрасли, таким как:
-
Избегайте отравления кэша: Если заголовок
Access-Control-Max-Ageсервера установлен на длительный срок, для последующих запросов могут использоваться устаревшие разрешения, что приведёт к потенциальным проблемам безопасности.Убедитесь, что
Access-Control-Max-Ageустановлен на умеренный срок (по умолчанию и рекомендуемый срок - пять секунд). -
Будьте явными: Не рекомендуется использовать символы подстановки
*. Вместо этого явно укажите источники, которым разрешён доступ к ресурсам сервера, используя заголовокAccess-Control-Allow-Origin. Также явно укажите разрешённые методы и заголовки, используяAccess-Control-Allow-MethodsиAccess-Control-Allow-Headers.Используйте символы подстановки только тогда, когда вы уверены, что API должен быть доступен из любого источника.
-
Добавляйте важные заголовки CORS: Убедитесь, что сервер включает все необходимые заголовки в ответе. Например, отсутствие заголовка
Access-Control-Allow-Credentialsможет вызвать проблемы, если браузер не передаёт учетные данные, когда это нужно. Если он установлен в неправильное значение, браузер может передавать учетные данные нежелательному источнику. -
Документируйте всё: Важно должным образом задокументировать политики CORS и сделать их понятными для разработчиков, которые могут интегрировать ваши API в своё приложение. Эта практика вызывает больше доверия со стороны пользователей вашего API.
- Проверка учетных данных: По умолчанию CORS игнорирует учетные данные. Если вам нужно включить учетные данные (такие как файлы cookie или аутентификацию HTTP) в кросс-доменные запросы, установите
Access-Control-Allow-Credentialsвtrue. Однако вы также должны убедиться, что сервер проверяет и обрабатывает учетные данные безопасно.
Читайте также: https://www.moesif.com/blog/technical/cors/Authoritative-Guide-to-CORS-Cross-Origin-Resource-Sharing-for-REST-APIs/