Сравнение event.origin с фиксированным строковым литералом с нормализованным портом (например, event.origin === 'https://app.example.com') подходит для простых белых списков. Однако сравнение строк вручную и регулярные выражения не работают при проверке произвольных URL, элементов link в DOM, переадресаций между субдоменами и сообщений в изолированной среде.
- Подмена префикса и подстроки. Проверка
url.startsWith('https://app.example.com')обходится с помощью'https://app.example.com.attacker.example'. - Ненормализованные URL. Полные URL с путями, строками запроса или явными портами по умолчанию (
'https://app.example.com:443/path') не соответствуют'https://app.example.com'при строковом равенстве без предварительного синтаксического анализа и нормализации. - Ошибки в списке общедоступных суффиксов. Разделение имен хостов по символу
.для проверки отношений между сайтами не работает для общедоступных суффиксов с несколькими метками (например,.co.ukили доменов общего хостинга, таких как.github.io) и игнорирует несоответствия схем (http:иhttps:). - Конфликты непрозрачных источников
"null". Изолированные iframe-объекты (<iframe sandbox="allow-scripts">) и URLdata:сериализуютevent.originв строку"null". Сравнение двух сериализованных источников среды выполнения с помощьюoriginA === originBошибочно приравнивает два несвязанных непрозрачных контекста ("null" === "null").
Origin API (Origin.from(), isSameOrigin(), isSameSite() и opaque) предоставляет структурированные, соответствующие спецификации сравнения источников и сайтов с одинаковой схемой, поддерживаемые синтаксическим анализатором URL браузера и списком Public Suffix List.
Как выбрать подходящее сравнение для вашего сценария
- Используйте
isSameOrigin()для строгих границ привилегий. Требуйте точного совпадения(scheme, host, port)при обработке межконтекстных командpostMessage(например, встроенного всплывающего окна оплаты или аутентификации) или при принятии решения о том, следует ли прикреплять учетные данные. Даже разные субдомены (https://blog.example.comиhttps://app.example.com) считаются разными источниками и должны отклоняться, чтобы уязвимость в одном субдомене не позволяла управлять другим. - Используйте
isSameSite()для навигации между субдоменами и классификации ссылок. Разрешите любой субдомен или порт, использующий ту же схему и регистрируемый домен, при проверке URL переадресации после входа (?returnTo=https://billing.example.co.uk/orders) или классификации ссылок как внутренних или внешних.isSameSite()отклоняет как понижение схемыhttp:, так и дочерние клиенты в общих общедоступных суффиксах (.co.uk,.github.io). - Используйте
origin.opaqueиOrigin.from(event)для непрозрачных или изолированных контекстов. Отклоняйте URLdata:для переадресации (origin.opaque === true) или закрепляйте непрозрачныйOrigin, извлеченный с помощьюOrigin.from(event), при взаимодействии с определенным виджетом<iframe sandbox="allow-scripts">, сериализованныйevent.originкоторого –"null".
1. Проверка точных источников для привилегированных действий (Origin.from() и isSameOrigin())
Используйте Origin.from(), чтобы извлечь объект Origin и сравнить его с доверенным Origin с помощью isSameOrigin():
- Обе стороны должны быть экземплярами
Origin.isSameOrigin(other)иisSameSite(other)принимают только объектOrigin. Если передать необработанную строку, возникнет ошибкаTypeError. - Поддерживаемые входные данные
Origin.from():- Сериализованные строки URL (например,
'https://app.example.com:443/path'). Обратите внимание, что любая недопустимая строка URL, например'example'или строка'null', сериализованнаяevent.originдля непрозрачных источников, приводит к тому, чтоOrigin.from()выдаетTypeError. - Объекты платформы, которые определяют шаги извлечения источника в спецификации HTML:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>),WindowилиWorkerGlobalScope(Origin.from(self)) того же источника и экземплярыMessageEvent, отправленные браузером (Origin.from(event)). - Объекты
Location,WorkerLocation,Document,RequestиResponseне определяют шаги извлечения источника и вызывают ошибкуTypeError, если передаются напрямую вOrigin.from(). Вместо этого передавайтеself(для текущего контекста) илиrequest.url/response.url.
- Сериализованные строки URL (например,
- Обрабатывайте как отправленные браузером, так и синтетические экземпляры
MessageEvent. Реальные события браузераpostMessageсодержат внутреннее происхождение, извлеченноеOrigin.from(event), тогда как синтетические событияnew MessageEvent('message', { origin }), созданные в модульных тестах, заполняют только строкуevent.originи вызывают ошибку вOrigin.from(event). Если вtry...catchиспользовать резервное значениеOrigin.from(event.origin), то будут корректно обрабатываться оба варианта, а недействительные строки и строки"null"будут отклоняться.
// Replace with your application's trusted origin(s)
const TRUSTED_ORIGIN = Origin.from('https://app.example.com');
function extractOrigin(candidate) {
try {
return Origin.from(candidate);
} catch (err) {
// Synthetic new MessageEvent('message', { origin }) objects in tests lack an
// internal browser origin; fall back to parsing candidate.origin if it is a URL.
if (typeof MessageEvent !== 'undefined' && candidate instanceof MessageEvent) {
return Origin.from(candidate.origin);
}
throw err;
}
}
export function isTrustedSameOrigin(candidate) {
try {
return extractOrigin(candidate).isSameOrigin(TRUSTED_ORIGIN);
} catch {
// Rejects invalid URLs, "null" strings, or unsupported objects
return false;
}
}
window.addEventListener('message', (event) => {
if (!isTrustedSameOrigin(event)) return;
// Safe to process privileged commands from the verified origin
});
2. Проверка переадресации и ссылок между субдоменами (isSameSite())
При проверке целей переадресации (например, параметров ?returnTo=) или внутренних ссылок между субдоменами одного регистрируемого домена (например, https://app.example.co.uk и https://billing.example.co.uk:8443) используйте isSameSite():
isSameSite()выполняет сравнение одинаковых сайтов с учетом схемы:https://sub.example.comиhttp://sub.example.comне считаются одинаковыми сайтами, потому что у них разные схемы.isSameSite()использует встроенный в браузер список общедоступных суффиксов, поэтому разные клиенты с общими общедоступными суффиксами (например,https://tenant-a.github.ioиhttps://tenant-b.github.ioилиhttps://a.co.ukиhttps://b.co.uk) оцениваются какfalse.
// Replace with your application's primary origin
const APP_ORIGIN = Origin.from('https://app.example.co.uk');
export function isAllowedSameSiteUrl(candidateUrlOrElement) {
try {
return extractOrigin(candidateUrlOrElement).isSameSite(APP_ORIGIN);
} catch {
return false;
}
}
// true: same scheme (https) and same registrable domain (example.co.uk)
isAllowedSameSiteUrl('https://billing.example.co.uk:8443/checkout');
// false: scheme downgrade (http vs. https) is rejected by schemeful same-site
isAllowedSameSiteUrl('http://billing.example.co.uk/checkout');
3. Различать и закреплять непрозрачные источники (origin.opaque и Origin.from(event))
Изолированные фреймы iframe (<iframe sandbox="allow-scripts">), URL data: и new Origin() создают непрозрачные источники (origin.opaque === true). Хотя их сериализация строк всегда имеет вид "null", Origin.from(event) извлекает фактический базовый непрозрачный источник отправителя из отправленного браузером MessageEvent:
- Два сообщения, отправленные из одного и того же изолированного iframe, возвращают объекты
Origin, которыеisSameOrigin()друг с другом (true). - Сообщения, отправленные из разных изолированных iframe или отдельных вызовов
Origin.from('data:...')/new Origin(), дают разные непрозрачные источники, которые оцениваютisSameOrigin()какfalse, предотвращая спуфинг"null" === "null"в несвязанных непрозрачных контекстах. new Origin()создает новый уникальный непрозрачный источник, который не соответствует ничему, кроме самого себя. Это полезно в качестве стандартного запрещающего все сторожа, прежде чем ожидаемый одноранговый источник будет закреплен один раз при инициализации.
// Initialize with a unique opaque sentinel that matches no other origin
let pinnedSandboxOrigin = new Origin();
let isSandboxPinned = false;
export function handleSandboxedWidgetMessage(event, expectedWidgetWindow) {
try {
const senderOrigin = Origin.from(event);
// Pin the opaque origin only once on initialization from the expected sandboxed iframe
if (!isSandboxPinned && event.source === expectedWidgetWindow && event.data?.type === 'init') {
pinnedSandboxOrigin = senderOrigin;
isSandboxPinned = true;
}
// Subsequent messages from the SAME sandboxed iframe match pinnedSandboxOrigin;
// messages from any other sandboxed iframe (also event.origin === "null") return false.
return senderOrigin.isSameOrigin(pinnedSandboxOrigin);
} catch {
return false;
}
}
Рекомендации
- РЕКОМЕНДУЕТСЯ преобразовывать обе стороны сравнения в экземпляры
Originс помощьюOrigin.from()перед вызовомisSameOrigin(other)илиisSameSite(other). - РЕКОМЕНДУЕТСЯ передавать отправленный браузером
MessageEventнапрямую вOrigin.from(event), когда нужно сохранить идентификатор скрытого источника, и использоватьOrigin.from(self)(а неLocationилиDocument, которые вызываютTypeError), чтобы получитьOriginтекущего контекста выполнения. - РЕКОМЕНДУЕТСЯ оборачивать вызовы
Origin.from()для ненадежных или динамических входных данных в блокtry...catch, чтобы недопустимые URL, строки"null"или неподдерживаемые объекты безопасно возвращалиfalseвместо того, чтобы вызывать необработанную ошибкуTypeError. - НЕ СРАВНИВАЙТЕ две строки
event.originилиurl.originнапрямую, не отклонив сначала"null"(origin !== 'null'), поскольку два несвязанных скрытых источника сериализуются в"null". - НЕ проверяйте источники или сайты с помощью
startsWith(),includes(),endsWith()или простого разделения имени хоста.split('.').
Резервная стратегия
Если ваша целевая аудитория Baseline включает браузеры, которые ещё не поддерживают интерфейс Origin, определите функцию 'Origin' in globalThis и вернитесь к синтаксическому анализу URL с помощью new URL():
- Резервный вариант для одного источника (источники кортежей). Извлеките строку URL (используя
event.originдля объектовMessageEventилиcandidate.hrefдля элементов ссылок), проанализируйте как кандидат, так и доверенные URL с помощьюnew URL(), отклоните непрозрачные источники"null"(candidateUrl.origin !== 'null') и сравнитеcandidateUrl.origin === trustedUrl.origin. Поскольку непрозрачный источник никогда не совпадает с источником-кортежем, например'https://app.example.com', немедленное отклонение'null'соответствуетOrigin.isSameOrigin(). - Резервный вариант для одного сайта (предупреждение). Интерфейс
URLне предоставляет доступ к списку публичных суффиксов браузера. В браузерах безOriginвыполняйте сопоставление суффиксов только с явным, известным частным регистрируемым доменом, который вы контролируете (проверяя совпадение какprotocol, так и точное или с префиксом в виде точкиhostname), или используйте поддерживаемую библиотеку Public Suffix List. - Резервный вариант для непрозрачных изолированных фреймов iframe. Браузеры без
Originне предоставляют отдельные идентификаторы непрозрачных источников вMessageEvent(event.originвсегда имеет значение"null"). Если вы намеренно принимаете сообщения из известного изолированного фрейма<iframe sandbox="allow-scripts">, проверяйтеevent.source === expectedWidgetWindow(или обменивайтесь данными через выделенныйMessageChannelMessagePort, переданный во фрейм iframe при инициализации), а не сравнивайтеevent.origin.
// Replace with your trusted reference URL and known registrable domain
const TRUSTED_URL = 'https://app.example.com';
const TRUSTED_SITE_DOMAIN = 'example.com';
function extractUrlString(candidate) {
if (typeof candidate === 'string') return candidate;
if (typeof MessageEvent !== 'undefined' && candidate instanceof MessageEvent) {
return candidate.origin;
}
if (candidate === globalThis) {
return globalThis.location?.href ?? '';
}
return candidate?.href ?? '';
}
function extractOrigin(candidate) {
try {
return Origin.from(candidate);
} catch (err) {
if (typeof MessageEvent !== 'undefined' && candidate instanceof MessageEvent) {
return Origin.from(candidate.origin);
}
throw err;
}
}
export function checkSameOriginWithFallback(candidate) {
try {
if ('Origin' in globalThis) {
return extractOrigin(candidate).isSameOrigin(Origin.from(TRUSTED_URL));
}
const candidateUrl = new URL(extractUrlString(candidate));
const trustedUrl = new URL(TRUSTED_URL);
return candidateUrl.origin !== 'null' && candidateUrl.origin === trustedUrl.origin;
} catch {
return false;
}
}
export function checkSameSiteWithFallback(candidate) {
try {
if ('Origin' in globalThis) {
return extractOrigin(candidate).isSameSite(Origin.from(TRUSTED_URL));
}
const candidateUrl = new URL(extractUrlString(candidate));
const trustedUrl = new URL(TRUSTED_URL);
return (
candidateUrl.origin !== 'null' &&
candidateUrl.protocol === trustedUrl.protocol &&
(candidateUrl.hostname === TRUSTED_SITE_DOMAIN ||
candidateUrl.hostname.endsWith(`.${TRUSTED_SITE_DOMAIN}`))
);
} catch {
return false;
}
}