Webrium
Подписаться

CORS: почему браузер блокирует запрос и что менять на сервере

Как браузер проверяет безопасность запросов и почему сервер должен правильно настраивать CORS

Схема совпадения заголовков CORS: разрешённый и заблокированный запросЗапрос разрешёнЗапрос заблокирован

Как браузер решает, пропустить запрос или заблокировать

Когда JavaScript на странице пытается отправить запрос на другой домен, браузер не передаёт его сразу серверу. Вместо этого он проводит серию проверок, чтобы понять, разрешён ли такой запрос. Эти проверки и называются CORS — Cross-Origin Resource Sharing. Механизм работает не как отдельный протокол, а как набор ограничений, которые браузер накладывает поверх HTTP.

Процесс выглядит так:

  1. Скрипт на странице вызывает fetch() или XMLHttpRequest с адресом другого домена.
  2. Браузер анализирует параметры запроса: метод, заголовки, наличие credentials (кук или токенов).
  3. Если запрос не подходит под критерии простого запроса, браузер сначала отправляет предварительный запрос OPTIONS на тот же адрес.
  4. Сервер отвечает заголовками, которые описывают, какие запросы он разрешает. Например:
    Access-Control-Allow-Origin: https://example.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: Content-Type, Authorization
  5. Браузер сравнивает эти заголовки с параметрами исходного запроса. Если всё совпадает — браузер пропускает ответ сервера к скрипту. Если нет — блокирует и показывает ошибку в консоли.

Ключевой момент здесь в том, что сервер должен не просто вернуть данные, а явно разрешить доступ к ним. Без этого браузер не отдаст ответ скрипту, даже если сервер его вернул. Это не баг, а защита: браузер не знает, доверяет ли пользователь этому другому домену, поэтому требует явного разрешения.

Какие запросы считаются простыми и почему это важно

Браузер не отправляет предварительный запрос OPTIONS для простых запросов. Простыми считаются те, которые не могут изменить данные на сервере или украсть чувствительную информацию. Критерии простого запроса:

  • Методы: только GET, HEAD или POST.
  • Заголовки: только те, которые браузер может добавить автоматически. Это Accept, Accept-Language, Content-Language и Content-Type с одним из трёх значений: text/plain, multipart/form-data или application/x-www-form-urlencoded.
  • Нет пользовательских заголовков (например, Authorization или X-Custom-Header).

Если запрос не подходит под эти условия, браузер сначала отправит OPTIONS, чтобы узнать, разрешает ли сервер такой запрос. Это называется предварительным запросом (preflight request). Если сервер не ответит правильными заголовками, браузер заблокирует основной запрос, даже не отправив его.

Почему это важно? Потому что многие разработчики сначала пробуют отправить запрос с пользовательскими заголовками или методами вроде PUT или DELETE, не понимая, что браузер сначала отправит OPTIONS. Если сервер не настроен отвечать на OPTIONS, запрос не пройдёт, даже если сервер готов обработать основной запрос.

Почему сервер должен отвечать на OPTIONS

Когда браузер отправляет предварительный запрос OPTIONS, сервер должен ответить заголовками, которые описывают, какие запросы он разрешает. Например:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Origin: https://example.com

Если сервер не отвечает на OPTIONS или отвечает неполными заголовками, браузер заблокирует основной запрос. При этом сервер может даже не знать, что запрос был заблокирован — браузер не отправляет основной запрос, если OPTIONS не прошёл.

Например, если скрипт на https://example.com пытается отправить PUT запрос на https://api.example.com, браузер сначала отправит OPTIONS. Если сервер не ответит заголовком Access-Control-Allow-Methods: PUT, браузер заблокирует основной запрос, даже если сервер готов его обработать.

Что менять на сервере, чтобы запросы проходили

1. Указать разрешенный источник

Самая частая ошибка — сервер не указывает Access-Control-Allow-Origin или указывает его неверно. Например, если скрипт на https://example.com пытается обратиться к https://api.example.com, сервер должен вернуть:

Access-Control-Allow-Origin: https://example.com

Если сервер должен разрешать доступ с нескольких доменов, нельзя просто перечислить их в заголовке через запятую. Вместо этого сервер должен проверять заголовок Origin из запроса и возвращать его же в Access-Control-Allow-Origin. Например, на Nginx это можно сделать так:

if ($http_origin ~* (https?://example\.com|https?://another\.com)) {
    set $cors "true";
}
if ($cors = "true") {
    add_header 'Access-Control-Allow-Origin' "$http_origin";
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
    add_header 'Access-Control-Allow-Credentials' 'true';
}

Если сервер возвращает Access-Control-Allow-Origin: *, это работает только для запросов без credentials. Если скрипт отправляет куки или токены, браузер требует явного указания домена.

2. Разрешить нужные методы и заголовки

Если скрипт использует нестандартные методы (например, PUT или DELETE), сервер должен явно разрешить их в заголовке Access-Control-Allow-Methods. То же касается заголовков: если скрипт добавляет пользовательские заголовки (например, Authorization или X-Custom-Header), сервер должен разрешить их в Access-Control-Allow-Headers.

Например, если скрипт отправляет запрос с заголовком Authorization, сервер должен вернуть:

Access-Control-Allow-Headers: Authorization

Если сервер не разрешит этот заголовок, браузер заблокирует запрос.

3. Обрабатывать предварительные запросы OPTIONS

Сервер должен корректно отвечать на предварительные запросы OPTIONS. В большинстве фреймворков это делается автоматически, но если сервер написан вручную, нужно добавить обработчик для OPTIONS. Например, на Express.js:

app.options('/api/data', (req, res) => {
    res.header('Access-Control-Allow-Origin', 'https://example.com');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.send();
});

Если сервер не отвечает на OPTIONS, браузер заблокирует основной запрос, даже если сервер готов его обработать.

4. Разрешить credentials, если нужны куки или токены

Если скрипт отправляет куки или токены в заголовке Authorization, сервер должен разрешить это с помощью:

Access-Control-Allow-Credentials: true

При этом Access-Control-Allow-Origin не может быть *, он должен быть явным доменом. Например:

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

Если сервер вернёт Access-Control-Allow-Origin: * вместе с Access-Control-Allow-Credentials: true, браузер заблокирует запрос, потому что wildcard несовместим с credentials.

Почему не работает wildcard (*) и когда его использовать

Заголовок Access-Control-Allow-Origin: * разрешает доступ с любого домена, но только для запросов без credentials. Если скрипт отправляет куки или токены, браузер требует явного указания домена в Access-Control-Allow-Origin. Wildcard в этом случае не работает.

Когда можно использовать wildcard? Только если:

  • Запрос не отправляет credentials (куки или токены).
  • Сервер не требует аутентификации.
  • Данные на сервере нечувствительные и доступны всем.

Например, если сервер отдаёт публичные данные, можно использовать wildcard:

Access-Control-Allow-Origin: *

Но если скрипт отправляет токен в заголовке Authorization, wildcard не сработает, и браузер заблокирует запрос.

Тупик: что пробуют первым и почему это не помогает

Чаще всего разработчики сначала пробуют добавить Access-Control-Allow-Origin: * и надеются, что этого достаточно. Но это не работает в большинстве случаев, потому что:

  1. Запрос не простой. Например, если скрипт использует метод PUT или добавляет пользовательский заголовок Authorization, браузер отправит предварительный запрос OPTIONS. Если сервер не отвечает на OPTIONS или не разрешает нужные методы и заголовки, запрос будет заблокирован.

  2. Скрипт отправляет credentials. Если скрипт отправляет куки или токены, wildcard * не работает, и браузер требует явного указания домена.

  3. Сервер не разрешает нужные методы или заголовки. Даже если Access-Control-Allow-Origin указан правильно, браузер заблокирует запрос, если сервер не разрешил нужные методы или заголовки.

Например, если скрипт отправляет запрос с методом DELETE, но сервер не разрешает его в Access-Control-Allow-Methods, браузер заблокирует запрос, даже если Access-Control-Allow-Origin указан правильно.

Как проверить, что сервер настроен правильно

Чтобы понять, почему запрос блокируется, можно использовать инструменты разработчика в браузере:

  1. Откройте вкладку Network в инструментах разработчика.
  2. Отправьте запрос и найдите его в списке.
  3. Посмотрите на предварительный запрос OPTIONS (если он был отправлен) и на основной запрос.
  4. Проверьте заголовки ответа сервера на наличие Access-Control-Allow-Origin, Access-Control-Allow-Methods и Access-Control-Allow-Headers.
  5. Сравните их с параметрами запроса. Если что-то не совпадает, браузер заблокирует запрос.

Например, если скрипт отправляет запрос с заголовком X-Custom-Header, но сервер не разрешает его в Access-Control-Allow-Headers, браузер покажет ошибку в консоли.

Альтернативы, если сервер не под вашим контролем

Если сервер не под вашим контролем и не поддерживает CORS, есть несколько обходных путей:

  1. Обратный прокси. Можно настроить свой сервер как прокси, который будет добавлять нужные заголовки CORS. Например, если скрипт на https://example.com пытается обратиться к https://api.another.com, можно настроить прокси на https://example.com/api, который будет проксировать запросы на https://api.another.com и добавлять заголовки CORS.

  2. JSONP. Это устаревший метод, который работает только для GET запросов. Сервер оборачивает данные в вызов функции JavaScript, и браузер выполняет этот код. Но JSONP небезопасен и не поддерживает современные методы вроде POST или PUT.

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

Лучший вариант — настроить сервер правильно, но если это невозможно, обратный прокси — самое надёжное решение.

Позиция редакции

CORS — это не баг, а необходимая защита, которая предотвращает кражу данных пользователя. Браузер блокирует запросы не потому, что ему так захотелось, а потому, что сервер не дал явного разрешения на доступ. Поэтому правильное решение — не обходить CORS, а настроить сервер так, чтобы он корректно отвечал на предварительные запросы и разрешал нужные методы и заголовки.

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

Единственное условие, при котором позиция неверна: если вы работаете в закрытой сети, где все запросы идут на доверенные домены, и безопасность не критична. В этом случае можно отключить CORS в браузере или использовать расширения, но это небезопасно для продакшена и не должно использоваться в публичных приложениях.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

На шкале времени жизни экрана величина — момент, когда становятся известны входные данные, — пробивает пороговую линию между сервером и клиентом: если данные изсервер/клиентвремя жизни экрана
Предыстория

Где проходит граница клиент — сервер

Как разделить клиент и сервер не по типам кода, а по моменту, когда известны данные — и почему это важно для CORS.

Прокси как граница: часть данных пересекает её и теряетсяснаруживнутри
Практика

Обратный прокси: что он решает и что ломает

Обратный прокси разгружает сервер, но добавляет свои риски — разбираем, что он исправляет и какие проблемы создаёт сам.

Две угрозы по разные стороны: одна закрывается хранилищем, другая — серверомчужой скриптчужой сайт
Углубление

Токен в браузере: где его хранить

Как выбрать хранилище для токена в браузере: разбор моделей угроз и компромиссов, которые не зависят от удобства.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть