CORS и разрешённые origin
Как приём событий ActionPulse проверяет origin браузерного запроса, что настроить в проекте, как ведёт себя preflight и какие директивы нужны в политике безопасности контента.
На этой странице
Когда события отправляет страница, а не бэкенд, в дело вступают две независимые проверки. Первая — обычный CORS браузера: он решает, позволено ли странице прочитать ответ с другого домена. Вторая — список разрешённых origin у браузерного токена: его проверяет сам приём и отказывает, если адрес страницы в списке не значится.
Различать их важно, потому что почти все «проблемы с CORS» в ActionPulse — это вторая проверка. Транспорт настроен так, чтобы браузер не мешал: запросы приёма отвечают разрешением для любого домена, а решение принимает сервер по типу ключа, области действия и origin.
Как приём проверяет origin
Проверка касается только браузерного токена. Приём берёт заголовок Origin
запроса, приводит его к каноническому виду и сравнивает на точное совпадение
со списком, заданным при выпуске токена.
Правила, зашитые в проверку:
- допустимы только схемы
httpиhttps; - маски запрещены — адрес вида
https://*.example.comне примут ни при выпуске, ни при сравнении; - в origin не может быть пути, параметров, фрагмента и имени пользователя;
- пустой список origin означает отказ всегда — токен без списка нерабочий;
- в списке от 1 до 32 адресов.
Что при сравнении не важно
Эти различия приводятся к одному виду и на результат не влияют:
| Запись | Приводится к |
|---|---|
HTTPS://Example.COM |
регистр схемы и хоста снимается |
https://example.com/ |
завершающий слеш убирается |
https://example.com. |
завершающая точка в имени хоста убирается |
https://example.com:443 |
порт по умолчанию для схемы убирается |
http://example.com:80 |
порт по умолчанию для схемы убирается |
Поддомен, схема и порт — разные origin
А эти различия важны, и каждое даёт отдельный origin:
| Пара адресов | Одно и то же? |
|---|---|
https://example.com и https://www.example.com |
нет, поддомен считается отдельно |
https://example.com и http://example.com |
нет, схема входит в origin |
https://example.com и https://example.com:8443 |
нет, непустой порт входит в origin |
https://shop.example.com и https://blog.example.com |
нет, перечисляйте оба |
Поэтому список нужно составлять по фактическим адресам, а не по домену:
production, вариант с www и без него, поддомены со своими страницами, адрес
предпросмотра сборок, адрес стенда разработки со схемой и портом.
Что настроить в проекте
Список origin задаётся один раз — при выпуске браузерного токена.
- Откройте Настройки → Проект, секция «Credentials проекта». Тот же выпуск есть на экране Настройки → Установка трекера, секция «Доступ к сбору данных».
- В блоке «Browser token» заполните поле «Разрешённые origins» — адреса через запятую или с новой строки — и нажмите «Выпустить browser token».
- Скопируйте полное значение токена сразу: повторно оно не показывается.
- Подставьте токен в тег или в вызов инициализации.
Ошибки ввода приходят сразу: пустой список, маска в адресе, адрес с путём, параметрами, фрагментом или логином и паролем. Проверки в кабинете и на сервере одинаковые, авторитетен сервер.
Выпуск токена доступен роли администратора и владельцу проекта. Подробнее о типах ключей и о том, что ещё привязано к токену, — в типах ключей.
Preflight и заголовки
Трекер отправляет события с заголовками Authorization и X-PP-Tracker,
поэтому браузер сначала делает предварительный запрос OPTIONS. Тип содержимого
при этом намеренно простой (text/plain), чтобы не добавлять к проверке ещё
одно условие.
Предварительный запрос обрабатывается до всякой авторизации и всегда
получает 204. Он не зависит ни от токена, ни от списка origin, ни от тарифа.
Практический вывод: если OPTIONS не получил ответа или получил ошибку, дело не
в настройках ActionPulse — смотрите на свой прокси, WAF или расширение
браузера.
Отдельный случай — отправка на выходе со страницы: там запрос уходит без заголовков, поэтому предварительная проверка ему не нужна и в сетевой панели её не будет. Публичный токен в этом случае едет в теле запроса.
Ответ на запросы приёма несёт:
| Заголовок | Значение |
|---|---|
Access-Control-Allow-Origin |
* |
Access-Control-Allow-Methods |
POST, OPTIONS |
Access-Control-Allow-Headers |
Authorization, Content-Type, X-PP-Tracker, X-PP-Source, X-PP-SDK-Name, X-PP-SDK-Version, X-PP-App-Version, X-PP-Release |
Access-Control-Max-Age |
600 — результат проверки браузер кэширует 10 минут |
Vary |
Origin |
Заголовок Access-Control-Allow-Credentials не выставляется никогда: приём не
использует cookie, и разрешение для любого домена не даёт сторонним сайтам
отправлять запросы от имени вошедшего пользователя.
Файлы трекера — отдельная история: версионные файлы и манифест отдаются с
разрешением для любого домена, разрешёнными GET, OPTIONS, заголовком
If-None-Match и открытым ETag. Именно поэтому к версионному адресу можно
применять crossorigin="anonymous" вместе с integrity. Для совместимого
адреса /v1/t.js проверка целостности недоступна в принципе — там нечего
фиксировать, содержимое не привязано к версии.
Если origin не совпал
Симптомов два, и оба тихие.
Запрос события получает 403:
{ "error": { "code": "credential_origin_forbidden",
"message": "browser credential origin is not allowed", "details": {} } }
Это не ошибка CORS. Ответ приходит с разрешением для любого домена, поэтому
страница его читает, а в консоли нет сообщения про заблокированный запрос.
Увидеть отказ можно только во вкладке «Сеть»: запрос завершён, код 403.
Трекер такой ответ не повторяет и считает партию доставленной — события просто
исчезают без единой записи в консоли.
Публичная конфигурация сбора отвечает «всё выключено»: запрос GET /v1/cfg
для несовпавшего origin получает 200 и заведомо безопасный ответ — автосбор
выключен, записи сессий выключены, доля записываемых сессий нулевая,
диагностика выключена. Тот же ответ приходит для неизвестного, отозванного или
истёкшего ключа: существование ключа не раскрывается. На странице это выглядит
так, будто автосбор просто «не настроен».
Что делать:
- Во вкладке «Сеть» откройте неудачный запрос на
/v1/trackи посмотрите заголовок запросаOrigin— это точное значение, с которым идёт сравнение. - Сравните его со столбцом «Scopes / origins» в списке ключей проекта.
- Если адрес отсутствует — выпустите новый токен с полным списком и замените значение. Отредактировать список у существующего токена нельзя.
Проверка одна и та же для всех запросов браузерного токена: /v1/identify,
запись сессий и захват элемента ответят тем же 403, если origin не в списке.
Ответ 403 с кодом credential_scope_forbidden — про другое: это несовпадение
типа ключа с запросом, а не origin. Разбор кодов по симптомам собран в
дереве диагностики.
Почему серверному ключу это не нужно
Проверка origin применяется только к браузерному токену. У запроса с бэкенда
заголовка Origin нет вовсе, и CORS к нему не относится: это не браузер.
Границей доверия для серверного ключа служит его секретность, а не адрес
источника.
Отсюда два практических следствия:
- если запрос с бэкенда получил
credential_origin_forbidden, значит там используется браузерный токен — замените его на серверный ключ; - перенести серверный ключ в браузер, «чтобы не настраивать origin», нельзя. Это не обход ограничения, а утечка: ключ станет виден каждому посетителю.
Политика безопасности контента
Если на сайте настроен Content-Security-Policy, разрешите три вещи:
| Директива | Значение | Кому нужно |
|---|---|---|
script-src |
origin, с которого загружается файл трекера | загрузка ядра и дополнительных модулей |
connect-src |
origin приёма событий | отправка событий, в том числе на выходе со страницы |
worker-src |
blob: — и data: для браузеров без blob-воркеров |
только записи сессий |
script-src 'self' https://YOUR_ACTIONPULSE_HOST;
connect-src 'self' https://YOUR_ACTIONPULSE_HOST;
worker-src 'self' blob:;
Если файлы вы отдаёте со своего домена, в script-src
достаточно 'self', а connect-src всё равно должен содержать адрес приёма.
Директива unsafe-eval не нужна ни при каких условиях: в бандлах нет ни eval,
ни new Function.
Дополнительные модули загружаются с origin самого файла трекера, а не с адреса
приёма событий: в script-src нужен именно тот домен, с которого приходит ядро.
Новую политику удобно сначала выкатить в режиме Content-Security-Policy-Report-Only
и посмотреть отчёты, а не выключать сбор целиком.
Что проверить
- Откройте страницу, подтвердите согласие и найдите во вкладке «Сеть» запрос на
/v1/track: ожидаемый ответ —202. - Проверьте предварительный запрос
OPTIONSна тот же адрес:204. Если его нет, браузер использует результат предыдущей проверки — он кэшируется на 10 минут. - Найдите запрос
GET /v1/cfgи посмотрите тело: если переключатели сбора включены в настройках проекта, а в ответе всё выключено, — origin не совпал. - Сверьте заголовок
Originзапроса со списком у токена; проверьте варианты сwwwи без него. - Убедитесь, что в консоли нет сообщений про
Content-Security-Policy, и отдельно проверьте записи сессий — они ломаются раньше остального.