Der Vergleich von event.origin mit einem festen, portnormalisierten Stringliteral (z. B. event.origin === 'https://app.example.com') funktioniert zwar für einfache Zulassungslisten, aber manuelle Stringvergleiche und reguläre Ausdrücke versagen bei der Validierung beliebiger URLs, DOM-Link-Elemente, subdomainübergreifender Weiterleitungen oder Nachrichten in einer Sandbox:
- Präfix- und Teilstring-Spoofing: Die Prüfung von
url.startsWith('https://app.example.com')wird durch'https://app.example.com.attacker.example'umgangen. - Nicht normalisierte URLs: Vollständige URLs mit Pfaden, Abfragestrings oder expliziten Standardports (
'https://app.example.com:443/path') stimmen nicht mit'https://app.example.com'überein, wenn sie nicht zuerst geparst und normalisiert werden. - Fehler bei der öffentlichen Suffixliste: Wenn Hostnamen bei
.aufgeteilt werden, um Same-Site-Beziehungen zu prüfen, schlägt dies bei öffentlichen Suffixen mit mehreren Labels (z. B..co.ukoder Domains mit gemeinsamem Hosting wie.github.io) fehl. Außerdem werden Abweichungen beim Schema (http:im Vergleich zuhttps:) ignoriert. - Kollisionen mit intransparenten
"null"-Ursprüngen: iFrames in der Sandbox (<iframe sandbox="allow-scripts">) unddata:-URLs serialisierenevent.originin den Literalstring"null". Wenn zwei serialisierte Laufzeitursprünge mitoriginA === originBverglichen werden, werden fälschlicherweise zwei nicht zusammenhängende intransparente Kontexte ("null" === "null") gleichgesetzt.
Die Origin API (Origin.from(), isSameOrigin(), isSameSite() und opaque) bietet strukturierte, spezifikationskonforme Vergleiche von Ursprüngen und schemabasierten Same-Site-Vergleichen, die vom URL-Parser des Browsers und der Public Suffix List unterstützt werden.
Den richtigen Vergleich für Ihr Szenario auswählen
isSameOrigin()für strenge Berechtigungsgrenzen verwenden: Erfordern Sie eine genaue(scheme, host, port)-Übereinstimmung, wenn Sie fensterübergreifendepostMessage-Befehle verarbeiten (z. B. ein eingebettetes Bezahl- oder Authentifizierungs-Pop-up) oder entscheiden, ob Anmeldedaten angehängt werden sollen. Sogar eine andere Subdomain (https://blog.example.comim Vergleich zuhttps://app.example.com) ist ein anderer Ursprung und muss abgelehnt werden, damit eine Sicherheitslücke in einer Subdomain nicht eine andere steuern kann.isSameSite()für die Navigation zwischen Subdomains und die Linkklassifizierung verwenden: Erlauben Sie jede Subdomain oder jeden Port, die dasselbe Schema und dieselbe registrierbare Domain verwenden, wenn Sie Weiterleitungs-URLs nach der Anmeldung (?returnTo=https://billing.example.co.uk/orders) validieren oder Links als intern oder extern klassifizieren.isSameSite()lehnt sowohl Downgrades deshttp:-Schemas als auch gleichrangige Mandanten auf gemeinsam genutzten öffentlichen Suffixen (.co.uk,.github.io) ab.origin.opaqueundOrigin.from(event)für opake oder Sandbox-Kontexte verwenden: Lehnedata:-URLs für Weiterleitungen (origin.opaque === true) ab oder pinne den opakenOrigin, der überOrigin.from(event)extrahiert wurde, wenn du mit einem bestimmten<iframe sandbox="allow-scripts">-Widget kommunizierst, dessen serialisierterevent.origin"null"ist.
1. Genaue Ursprünge für privilegierte Aktionen prüfen (Origin.from() und isSameOrigin())
Verwenden Sie Origin.from(), um ein Origin-Objekt zu extrahieren und es mit isSameOrigin() mit einem vertrauenswürdigen Origin zu vergleichen:
- Beide Seiten müssen
Origin-Instanzen sein:isSameOrigin(other)undisSameSite(other)akzeptieren nur einOrigin-Objekt. Wenn Sie einen Rohstring übergeben, wird einTypeErrorausgegeben. - Unterstützte
Origin.from()-Eingaben:- Serialisierte URL-Strings (z. B.
'https://app.example.com:443/path'). Beachten Sie, dass jeder ungültige URL-String, z. B.'example'oder der vonevent.originfür nicht transparente Ursprünge serialisierte String'null', dazu führt, dassOrigin.from()einenTypeErrorauslöst. - Plattformobjekte, die Schritte zur Ursprungsextraktion in der HTML-Spezifikation definieren:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>),WindowoderWorkerGlobalScope(Origin.from(self)) mit demselben Ursprung und vom Browser gesendeteMessageEvent-Instanzen (Origin.from(event)). - Objekte wie
Location,WorkerLocation,Document,RequestundResponsedefinieren keine Schritte zur Ursprungsextraktion und lösen einenTypeErroraus, wenn sie direkt anOrigin.from()übergeben werden. Übergeben Sie stattdessenself(für den aktuellen Kontext) oderrequest.url/response.url.
- Serialisierte URL-Strings (z. B.
- Sowohl vom Browser gesendete als auch synthetische
MessageEvent-Instanzen verarbeiten: EchtepostMessage-Browserereignisse enthalten einen internen Ursprung, der vonOrigin.from(event)extrahiert wird. Synthetischenew MessageEvent('message', { origin })-Ereignisse, die in Unit-Tests erstellt werden, füllen nur denevent.origin-String und lösen einen Fehler beiOrigin.from(event)aus. Wenn Sie innerhalb einestry...catch-Blocks aufOrigin.from(event.origin)zurückgreifen, werden beide Fälle sauber verarbeitet, während ungültige oder"null"-Strings weiterhin abgelehnt werden.
// 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. Subdomainübergreifende Weiterleitungen und Links validieren (isSameSite())
Verwenden Sie isSameSite(), wenn Sie Weiterleitungsziele (z. B. ?returnTo=-Parameter) oder interne Links über Subdomains derselben registrierbaren Domain (z. B. https://app.example.co.uk und https://billing.example.co.uk:8443) validieren:
isSameSite()führt einen Schemeful-Same-Site-Vergleich durch:https://sub.example.comundhttp://sub.example.comsind nicht Same-Site, da sich ihre Schemata unterscheiden.isSameSite()verwendet die im Browser integrierte öffentliche Suffixliste, sodass unterschiedliche Mandanten mit gemeinsamen öffentlichen Suffixen (z. B.https://tenant-a.github.ioundhttps://tenant-b.github.iooderhttps://a.co.ukundhttps://b.co.uk) alsfalseausgewertet werden.
// 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. Nicht transparente Ursprünge unterscheiden und anpinnen (origin.opaque und Origin.from(event))
Sandboxed iFrames (<iframe sandbox="allow-scripts">), data:-URLs und new Origin() erzeugen intransparente Ursprünge (origin.opaque === true). Während ihre String-Serialisierung immer "null" ist, extrahiert Origin.from(event) den tatsächlichen zugrunde liegenden intransparenten Ursprung des Absenders aus einem vom Browser gesendeten MessageEvent:
- Zwei Nachrichten, die vom selben iFrame in der Sandbox gesendet werden, ergeben
Origin-Objekte, die miteinanderisSameOrigin()sind (true). - Nachrichten, die von verschiedenen iFrames in einer Sandbox oder separaten
Origin.from('data:...')- odernew Origin()-Aufrufen gesendet werden, führen zu unterschiedlichen opaken Ursprüngen, dieisSameOrigin()zufalseauswerten. Dadurch wird"null" === "null"-Spoofing in nicht zusammenhängenden opaken Kontexten verhindert. new Origin()erstellt einen neuen, eindeutigen, intransparenten Ursprung, der nur mit sich selbst übereinstimmt. Das ist nützlich als Standard-Sentinel, der alles ablehnt, bevor ein erwarteter Peer-Ursprung einmal bei der Initialisierung angepinnt wird.
// 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;
}
}
Best Practices
- DO convert both sides of a comparison to
Origininstances viaOrigin.from()before callingisSameOrigin(other)orisSameSite(other). - EMPFOHLEN: Übergeben Sie ein vom Browser gesendetes
MessageEventdirekt anOrigin.from(event), wenn Sie die Identität des opaken Ursprungs beibehalten müssen, und verwenden SieOrigin.from(self)(anstelle vonLocationoderDocument, dieTypeErrorauslösen), um dasOrigindes aktuellen Ausführungskontexts abzurufen. - DO wrap
Origin.from()calls on untrusted or runtime inputs in atry...catchblock so invalid URLs,"null"strings, or unsupported objects safely returnfalseinstead of throwing an uncaughtTypeError. - Vergleichen Sie NICHT zwei Laufzeit-
event.origin- oderurl.origin-Strings direkt, ohne zuerst"null"(origin !== 'null') abzulehnen, da zwei nicht zusammenhängende opake Ursprünge beide zu"null"serialisiert werden. - NICHT Ursprünge oder Websites mit
startsWith(),includes(),endsWith()oder einfachem.split('.')-Hostnamen-Slicing validieren.
Fallback-Strategie
Wenn Ihr Baseline-Ziel Browser umfasst, die die Origin-Schnittstelle noch nicht unterstützen, sollten Sie die Funktion 'Origin' in globalThis erkennen und auf das Parsen von URLs mit new URL() zurückgreifen:
- Same-Origin-Fallback (Tupel-Ursprünge): Extrahieren Sie den URL-String (mit
event.originfürMessageEvent-Objekte odercandidate.hreffür Link-Elemente), parsen Sie sowohl die Kandidaten- als auch die vertrauenswürdigen URLs mitnew URL(), lehnen Sie"null"opake Ursprünge (candidateUrl.origin !== 'null') ab und vergleichen SiecandidateUrl.origin === trustedUrl.origin. Da ein opaker Ursprung nie denselben Ursprung wie ein Tupel-Ursprung wie'https://app.example.com'hat, entspricht die sofortige Ablehnung von'null'derOrigin.isSameOrigin(). - Same-Site-Fallback (Einschränkung): Die
URL-Schnittstelle stellt die öffentliche Suffixliste des Browsers nicht bereit. In Browsern ohneOriginsollten Sie nur einen Suffixabgleich mit einer expliziten, bekannten privaten registrierbaren Domain durchführen, die Sie kontrollieren (wobei sowohlprotocolals auch eine genaue oder mit einem Punkt vorangestelltehostname-Übereinstimmung überprüft werden). Alternativ können Sie eine gepflegte Public Suffix List-Bibliothek verwenden. - Undurchsichtiger/Sandbox-Iframe-Fallback: Browser ohne
Originstellen keine eindeutigen undurchsichtigen Ursprungsidentitäten fürMessageEventbereit (event.originist immer"null"). Wenn Sie absichtlich Nachrichten von einer bekannten Sandbox-<iframe sandbox="allow-scripts">akzeptieren, prüfen Sieevent.source === expectedWidgetWindow(oder kommunizieren Sie über einen dediziertenMessageChannelMessagePort, der bei der Initialisierung an den Iframe übertragen wird), anstattevent.originzu vergleichen.
// 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;
}
}