Porównywanie event.origin ze stałym, znormalizowanym pod kątem portu ciągiem znaków (np. event.origin === 'https://app.example.com') działa w przypadku prostych list dozwolonych, ale ręczne porównywanie ciągów znaków i wyrażenia regularne nie sprawdzają się podczas sprawdzania dowolnych adresów URL, elementów linków DOM, przekierowań między subdomenami czy wiadomości w piaskownicy:
- Podszywanie się pod prefiks i podłańcuch: sprawdzanie
url.startsWith('https://app.example.com')jest pomijane przez'https://app.example.com.attacker.example'. - Nieznormalizowane adresy URL: pełne adresy URL ze ścieżkami, ciągami zapytania lub jawnymi portami domyślnymi (
'https://app.example.com:443/path') nie pasują do'https://app.example.com'w przypadku równości ciągów znaków bez wcześniejszego przeanalizowania i znormalizowania. - Błędy na liście domen publicznych: dzielenie nazw hostów na
.w celu sprawdzenia relacji w tej samej witrynie nie działa w przypadku domen publicznych z wieloma etykietami (takich jak.co.uklub współdzielone domeny hostingowe, np..github.io) i ignoruje niezgodności schematów (http:ihttps:). - Kolizje nieprzezroczystych źródeł
"null": elementy iframe w piaskownicy (<iframe sandbox="allow-scripts">) i adresy URLdata:serializująevent.origindo ciągu tekstowego"null". Porównanie 2 zserializowanych źródeł środowiska wykonawczego za pomocąoriginA === originBbłędnie zrównuje 2 niepowiązane nieprzezroczyste konteksty ("null" === "null").
Interfejs API Origin (Origin.from(), isSameOrigin(), isSameSite() i opaque) zapewnia ustrukturyzowane, zgodne ze specyfikacją porównania pochodzenia i schematu tej samej witryny, które są obsługiwane przez parser adresów URL przeglądarki i listę sufiksów publicznych.
Wybór odpowiedniego porównania dla danego scenariusza
- Używaj
isSameOrigin()do określania ścisłych granic uprawnień: wymagaj dokładnego dopasowania(scheme, host, port)podczas obsługi poleceńpostMessagemiędzy oknami (takich jak osadzona płatność lub wyskakujące okienko uwierzytelniania) lub decydowania, czy dołączyć dane logowania. Nawet inna subdomena (https://blog.example.comihttps://app.example.com) jest innym źródłem i musi zostać odrzucona, aby luka w zabezpieczeniach jednej subdomeny nie mogła kontrolować innej. - Używaj
isSameSite()do nawigacji między subdomenami i klasyfikacji linków: zezwalaj na dowolną subdomenę lub port, które mają ten sam schemat i rejestrowalną domenę, podczas sprawdzania adresów URL przekierowań po zalogowaniu (?returnTo=https://billing.example.co.uk/orders) lub klasyfikowania linków jako wewnętrznych lub zewnętrznych.isSameSite()odrzuca zarówno obniżenie schematuhttp:, jak i dzierżawy równorzędne we wspólnych sufiksach publicznych (.co.uk,.github.io). - Używaj
origin.opaqueiOrigin.from(event)w przypadku nieprzezroczystych lub izolowanych kontekstów: odrzucaj adresy URLdata:w przypadku przekierowań (origin.opaque === true) lub przypinaj nieprzezroczysteOriginwyodrębnione za pomocąOrigin.from(event)podczas komunikacji z określonym widżetem<iframe sandbox="allow-scripts">, którego zserializowanyevent.originto"null".
1. Weryfikuj dokładne źródła działań uprzywilejowanych (Origin.from() i isSameOrigin())
Użyj Origin.from(), aby wyodrębnić obiekt Origin i porównać go z zaufanym obiektem Origin za pomocą isSameOrigin():
- Obie strony muszą być instancjami
Origin:isSameOrigin(other)iisSameSite(other)akceptują tylko obiektOrigin– przekazanie nieprzetworzonego ciągu znaków powoduje błądTypeError. - Obsługiwane dane wejściowe
Origin.from():- Zserializowane ciągi adresów URL (np.
'https://app.example.com:443/path'). Pamiętaj, że każdy nieprawidłowy ciąg adresu URL – np.'example'lub ciąg'null'zserializowany przezevent.originw przypadku nieprzezroczystych źródeł – powoduje, żeOrigin.from()zgłasza błądTypeError. - Obiekty platformy, które definiują kroki wyodrębniania źródła w specyfikacji HTML:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>), instancjeWindowlubWorkerGlobalScope(Origin.from(self)) z tego samego źródła oraz instancjeMessageEventwysyłane przez przeglądarkę (Origin.from(event)). - Obiekty takie jak
Location,WorkerLocation,Document,RequestiResponsenie definiują kroków wyodrębniania źródła i zwracają błądTypeError, jeśli zostaną przekazane bezpośrednio doOrigin.from(). Zamiast tego przekażself(w przypadku bieżącego kontekstu) lubrequest.url/response.url.
- Zserializowane ciągi adresów URL (np.
- Obsługuj zarówno instancje
MessageEventwysyłane przez przeglądarkę, jak i instancje syntetyczne: prawdziwe zdarzeniapostMessageprzeglądarki zawierają wewnętrzne źródło wyodrębnione przezOrigin.from(event), podczas gdy syntetyczne zdarzenianew MessageEvent('message', { origin })utworzone w testach jednostkowych wypełniają tylko ciągevent.origini wywołują wyjątek wOrigin.from(event). ZastosowanieOrigin.from(event.origin)w ramachtry...catchpozwala obsłużyć oba te przypadki, a jednocześnie odrzucać nieprawidłowe ciągi znaków lub ciągi znaków"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. Sprawdzanie przekierowań i linków między subdomenami (isSameSite())
Podczas sprawdzania poprawności celów przekierowania (takich jak parametry ?returnTo=) lub linków wewnętrznych w subdomenach tej samej domeny, którą można zarejestrować (np. https://app.example.co.uk i https://billing.example.co.uk:8443), używaj isSameSite():
isSameSite()przeprowadza porównanie schemeful same-site:https://sub.example.comihttp://sub.example.comnie są tymi samymi witrynami, ponieważ ich schematy się różnią.isSameSite()używa wbudowanej w przeglądarkę listy domen publicznych, więc różne dzierżawy we wspólnych domenach publicznych (takich jakhttps://tenant-a.github.ioihttps://tenant-b.github.iolubhttps://a.co.ukihttps://b.co.uk) są oceniane jakofalse.
// 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. Rozróżnianie i przypinanie nieprzezroczystych źródeł (origin.opaque i Origin.from(event))
Elementy iframe w trybie piaskownicy (<iframe sandbox="allow-scripts">), adresy URL data: i new Origin() generują nieprzezroczyste źródła (origin.opaque === true). Ich serializacja ciągów znaków zawsze wynosi "null", ale Origin.from(event) wyodrębnia rzeczywiste nieprzezroczyste źródło nadawcy z wysłanego przez przeglądarkę MessageEvent:
- Dwie wiadomości wysłane z tego samego elementu iframe w piaskownicy dają obiekty
Origin, które sąisSameOrigin()(true). - Wiadomości wysyłane z różnych elementów iframe w trybie piaskownicy lub oddzielne wywołania
Origin.from('data:...')/new Origin()dają różne nieprzezroczyste źródła, które są oceniane jakoisSameOrigin()dofalse, co zapobiega podszywaniu się pod"null" === "null"w niepowiązanych nieprzezroczystych kontekstach. new Origin()tworzy nowe, unikalne, nieprzezroczyste źródło, które nie pasuje do niczego poza sobą. Jest to przydatne jako domyślny strażnik odrzucający wszystko, zanim oczekiwane źródło równorzędne zostanie przypięte raz podczas inicjowania.
// 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;
}
}
Sprawdzone metody
- ZALECENIE: przed wywołaniem
isSameOrigin(other)lubisSameSite(other)przekonwertuj obie strony porównania na instancjeOriginza pomocąOrigin.from(). - PRZEKAZUJ obiekt
MessageEventwysłany przez przeglądarkę bezpośrednio doOrigin.from(event), gdy musisz zachować nieprzezroczystą tożsamość źródła, i używajOrigin.from(self)(a nieLocationaniDocument, które wywołująTypeError), aby uzyskaćOriginbieżącego kontekstu wykonywania. - ZALECENIE: wywołania
Origin.from()dotyczące niezaufanych lub środowiskowych danych wejściowych umieszczaj w blokutry...catch, aby nieprawidłowe adresy URL, ciągi znaków"null"lub nieobsługiwane obiekty bezpiecznie zwracałyfalsezamiast nieprzechwyconego błęduTypeError. - NIE porównuj bezpośrednio 2 ciągów znaków
event.originluburl.originśrodowiska wykonawczego bez wcześniejszego odrzucenia"null"(origin !== 'null'), ponieważ 2 niepowiązane nieprzezroczyste źródła są serializowane do"null". - NIE weryfikuj źródeł ani witryn za pomocą
startsWith(),includes(),endsWith()ani naiwnego dzielenia nazwy hosta.split('.').
Strategia zastępcza
Jeśli Twoja grupa docelowa korzysta z przeglądarek, które nie obsługują jeszcze interfejsu Origin, wykryj funkcję 'Origin' in globalThis i wróć do analizowania adresów URL za pomocą new URL():
- Zastępcze źródło z tej samej domeny (źródła krotek): wyodrębnij ciąg adresu URL (za pomocą
event.originw przypadku obiektówMessageEventlubcandidate.hrefw przypadku elementów linku), przeanalizuj adresy URL kandydatów i zaufane adresy URL za pomocąnew URL(), odrzuć nieprzezroczyste źródła"null"(candidateUrl.origin !== 'null') i porównajcandidateUrl.origin === trustedUrl.origin. Ponieważ nieprzezroczyste źródło nigdy nie jest tym samym źródłem co źródło krotki, takie jak'https://app.example.com', natychmiastowe odrzucenie'null'pasuje doOrigin.isSameOrigin(). - Zastępcza domena SameSite (zastrzeżenie): interfejs
URLnie udostępnia listy sufiksów publicznych przeglądarki. W przeglądarkach bezOrigindopasowuj sufiksy tylko do jawnej, znanej, prywatnej domeny możliwej do zarejestrowania, którą kontrolujesz (weryfikując zarównoprotocol, jak i dokładne dopasowanie lub dopasowanie z prefiksem kropkihostname), lub używaj utrzymywanej biblioteki Public Suffix List. - Zastępcza kreacja zastępcza nieprzezroczystego elementu iframe / elementu iframe w piaskownicy: przeglądarki bez
Originnie ujawniają odrębnych nieprzezroczystych tożsamości pochodzenia wMessageEvent(event.originto zawsze"null"). Jeśli celowo akceptujesz wiadomości ze znanego elementu<iframe sandbox="allow-scripts">w piaskownicy, zweryfikujevent.source === expectedWidgetWindow(lub komunikuj się przez dedykowany elementMessageChannelMessagePortprzeniesiony do elementu iframe podczas inicjowania), zamiast porównywać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;
}
}