Назад к блогу
Платформа

Границы CORS в браузере: заголовки, preflight, учетные данные и кэш

Практическая диагностика cross-origin запросов без смешения CORS с сетью, авторизацией, CSP и политикой разрешений.

BotBrowser Team

Документация

Нужна структурированная документация по теме Платформа?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

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

Cross-Origin Resource Sharing (CORS) - протокол, по которому браузер решает, может ли JavaScript прочитать ответ другого origin. Он не делает маршрут доступным, не выдает серверную авторизацию и не превращает сторонний API в доверенный. Поэтому HTTP 200 и ошибка CORS могут быть одновременными: сеть получила ответ, а Fetch скрыл его от страницы. Фиксируйте отдельно URL, origin страницы, статус, заголовки, сообщение консоли и видимый статус приложения. Нормативные детали находятся в [протоколе CORS Fetch](https://fetch.spec.whatwg.org/#http-cors-protocol) и практическом [руководстве MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). ## Origin и простые запросы Origin состоит из схемы, хоста и порта. В локальном fixture страница и API получают разные порты localhost во время запуска. Для читаемого ответа сервер возвращает `Access-Control-Allow-Origin: `. Браузер сравнивает его с origin отправителя; скрипт не может свободно подделать `Origin`. Запросы `GET`, `HEAD` или `POST` с safelisted заголовками иногда не требуют preflight, но ответ все равно должен разрешать CORS. `mode: 'no-cors'` дает opaque response, а не обход: тело и большинство заголовков недоступны. `mode: 'same-origin'` отклоняет cross-origin назначение. ## Preflight `Authorization`, JSON, нестандартный метод или заголовок обычно вызывают предварительный `OPTIONS`: ```http Origin: Access-Control-Request-Method: POST Access-Control-Request-Headers: authorization, content-type ``` Совместимый ответ содержит origin, метод и заголовки: ```http Access-Control-Allow-Origin: Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: authorization, content-type Access-Control-Max-Age: 300 Vary: Origin ``` Браузер проверяет preflight до реального запроса. Редирект, требование авторизации или ошибка 4xx у `OPTIONS` могут остановить поток. `Allow-Methods` не дает право пользователю выполнить операцию: проверка полномочий остается на endpoint. Кэш preflight способен сохранять старую политику, поэтому для проверки используйте новый контекст. ## Учетные данные и кэш Для cross-origin cookies нужен `учётные данные: 'include'`, явный origin и `Access-Control-Allow-Credentials: true`. `*` несовместим с читаемым credentialed ответом. `SameSite`, `Secure`, область cookie, авторизация и CSRF-защита независимы от CORS. Если сервер выбирает Allow-Origin динамически, добавьте `Vary: Origin`, чтобы общий CDN не отдал ответ одного origin другому. Учитывайте кэш браузера, preflight, фоновой обработчик, proxy и CDN. Детерминированный тест использует новый BrowserContext и реальные заголовки ответа; не меняйте production-кэш ради зеленого теста. ## Различие CORS, CSP и политикой разрешений | Контроль | Вопрос | Наблюдение | Что не доказывает | | --- | --- | --- | --- | | Сеть и DNS | Доступен ли URL? | DNS, TLS, соединение | Что ответ читаем или разрешен | | CORS | Может ли JavaScript прочитать ответ? | `Access-Control-Allow-*`, Fetch | Серверную авторизацию и бизнес-результат | | CSP | Какие назначения разрешены документу? | `connect-src`, отчеты | Разрешение CORS | | политикой разрешений | Может ли документ или iframe использовать функцию? | Политика и `allow` | Разрешение пользователя, устройство или CORS | [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) может заблокировать `connect-src` до ответа CORS. [политикой разрешений](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy) управляет функциями браузера в документе и iframe, но не открывает тело API. Не ослабляйте один контроль, чтобы скрыть ошибку другого. ## Собственный control/candidate fixture Разместите собственный endpoint `https://api.example.test/cors-fixture` и страницы `https://app.example.test/control` и `https://app.example.test/candidate`. Используйте одинаковые маршрут и синтетическое тело. Только control получает `Access-Control-Allow-Origin: `; candidate намеренно не получает. Кнопка запускает fetch, а элемент статуса виден пользователю. ```js import { chromium } from 'playwright'; import http from 'node:http'; import assert from 'node:assert/strict';

let pageOrigin = ''; let apiOrigin = ''; const observations = []; const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const close = server => new Promise(resolve => server.close(resolve)); const api = http.createServer((request, response) => { const name = request.url.slice(1); if (name === 'control') response.setHeader('Access-Control-Allow-Origin', pageOrigin); observations.push({ name, status: 200, allowOrigin: response.getHeader('Access-Control-Allow-Origin') || null }); response.writeHead(200, { 'Content-Type': 'application/json' }); response.end('{"fixture":true}'); }); const pageServer = http.createServer((request, response) => { const name = request.url.slice(1); response.end(<button>Run CORS check</button><output data-testid="cors-status"></output><script> document.querySelector('button').onclick = async () => { try { await fetch('${apiOrigin}/${name}'); document.querySelector('output').textContent = 'CORS_ALLOWED'; } catch { document.querySelector('output').textContent = 'FETCH_UNCLASSIFIED'; } };</script>); }); await listen(api); await listen(pageServer); const apiPort = api.address().port; const pagePort = pageServer.address().port; pageOrigin = http://127.0.0.1:${pagePort}; apiOrigin = http://127.0.0.1:${apiPort}; const browser = await chromium.launch(); const context = await browser.newContext(); context.setDefaultNavigationTimeout(8_000); async function runCase(name) { const page = await context.newPage(); try { await page.goto(${pageOrigin}/${name}); await page.getByRole('button', { name: 'Run CORS check' }).click(); await page.locator('[data-testid="cors-status"]').waitFor({ state: 'visible', timeout: 5_000 }); return await page.locator('[data-testid="cors-status"]').textContent(); } catch { return 'UNKNOWN'; } finally { await page.close(); } } try { const control = await runCase('control'); const candidate = await runCase('candidate'); assert.equal(control, 'CORS_ALLOWED'); assert.equal(candidate, 'FETCH_UNCLASSIFIED'); assert.deepEqual(observations.map(({ name, allowOrigin }) => ({ name, allowOrigin })), [ { name: 'control', allowOrigin: pageOrigin }, { name: 'candidate', allowOrigin: null } ]); } finally { await context.close(); await browser.close(); await close(pageServer); await close(api); }

Control должен показать `CORS_ALLOWED` после чтения синтетического тела. Candidate показывает `FETCH_UNCLASSIFIED` после отклонения Fetch; одного этого состояния недостаточно для CORS. Подтверждайте `CORS_BLOCKED`, только если лог собственного API показывает оба запроса и одинаковое тело со статусом 200, ответ candidate намеренно не содержит Allow-Origin, а control прочитал тот же endpoint с точным origin страницы.

Ошибка навигации, статуса или сервера остается `UNKNOWN`; ее нельзя приписывать сети или CORS. Навигация ограничена восемью секундами, каждая page закрывается при cleanup.

## Таблица диагностики

| Наблюдение                          | Первый владелец  | Проверка                                      | Осторожный вывод                          |
| ----------------------------------- | ---------------- | --------------------------------------------- | ----------------------------------------- |
| DNS, TLS или соединение не работают | Сеть             | Маршрут, proxy, сервис                        | Ответ CORS не получен                     |
| Preflight 4xx/5xx                   | API              | OPTIONS, auth, методы, заголовки              | Предварительный обмен не прошел           |
| 200, но скрипт не читает тело       | API/браузер      | Origin, режим учётных данных, exposed headers | CORS sharing не разрешен или ответ opaque |
| Cookie отсутствует                  | Auth/браузер     | режим учётных данных, SameSite, Secure        | CORS не делает cookie допустимой          |
| Разные результаты для origin        | API/кэш          | Allow-list и `Vary: Origin`                   | Политика или кэш отличаются               |
| Нарушение CSP                       | Веб-безопасность | `connect-src`, отчеты                         | Это блок CSP, не CORS                     |

Граница BotBrowser
Управляемые BrowserContext BotBrowser могут запускать авторизованные собственные control/candidate fixtures, изолировать синтетические cookies и хранилище и сравнивать видимые результаты Fetch на объявленной сборке браузера. Граница описана в [документации multi-account isolation](https://botbrowser.io/docs/identity/multi-account-isolation/).

Для проверки CORS BotBrowser может выполнить сравнение control/candidate, но не может настроить ответ API или обойти правило совместного использования браузера.
BotBrowser не пишет и не развертывает CORS-заголовки, не выдает серверную авторизацию, не исправляет сторонний API, не меняет origin и не обходит CORS браузера. Он также не доказывает завершение удаленной бизнес-операции. Сервер, CDN, учетные данные, CSP, политикой разрешений и состояние приложения принадлежат их владельцам.
Рабочий список

1. Опишите origin страницы и API, метод, заголовки, режим учётных данных и ожидаемый результат.
2. Проверьте DNS, TLS, proxy и маршрут до анализа policy-ошибки.
3. Запишите Origin, preflight, статус, CORS-заголовки, `Vary` и кэш.
4. Отделяйте CORS от CSP, политикой разрешений, пользовательского разрешения и авторизации.
5. Используйте собственный fixture с timeout навигации 8 секунд, статуса 5 секунд и cleanup.
6. Не смешивайте сетевую ошибку, timeout, ошибку preflight и CORS block.
   **Автор:** BotBrowser Team / BotBrowser команда
   См. [руководство по cross-origin isolation](/ru/blog/cross-origin-isolation-and-shared-memory-requirements/) и [руководство политикой разрешений](/ru/blog/permissions-policy-for-embedded-browser-features/).

## Sources

- Fixture URL (example only): https://app.example.test`
- Fixture URL (example only): https://app.example.test/control`

- [Протокол CORS Fetch](https://fetch.spec.whatwg.org/#http-cors-protocol)
- [CORS в MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
- [CSP в MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
- [политикой разрешений в MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy)
#CORS#Cross-Origin#Fetch#Веб-Безопасность

Переведите BotBrowser из исследований в продакшн

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