Hoewel het vergelijken van event.origin met een vaste, poortgenormaliseerde letterlijke tekenreeks (zoals event.origin === 'https://app.example.com') werkt voor eenvoudige toelatingslijsten, werken handmatige tekenreeksvergelijkingen en reguliere expressies niet meer bij de validatie van willekeurige URL's, DOM-linkelementen, omleidingen tussen subdomeinen of berichten in de sandbox:
- Voorvoegsel- en substringspoofing: De controle van
url.startsWith('https://app.example.com')wordt omzeild door'https://app.example.com.attacker.example'. - Niet-genormaliseerde URL's: Volledige URL's met paden, querytekenreeksen of expliciete standaardpoorten (
'https://app.example.com:443/path') komen niet overeen met'https://app.example.com'onder tekenreeksovereenkomst zonder eerst te parseren en te normaliseren. - Fouten in de lijst met openbare achtervoegsels: Hostnamen splitsen op
.om relaties op dezelfde site te controleren, mislukt bij openbare achtervoegsels met meerdere labels (zoals.co.ukof gedeelde hostingdomeinen zoals.github.io) en negeert niet-overeenkomende schema's (http:versushttps:). - Ondoorzichtige
"null"-oorsprongbotsingen: Sandboxed iframes (<iframe sandbox="allow-scripts">) endata:-URL's serialiserenevent.originnaar de letterlijke tekenreeks"null". Als je 2 geserialiseerde runtime-oorsprongen vergelijkt metoriginA === originB, worden 2 niet-gerelateerde ondoorzichtige contexten ("null" === "null") ten onrechte gelijkgesteld.
De Origin API (Origin.from(), isSameOrigin(), isSameSite() en opaque) biedt gestructureerde, aan de specificaties voldoenende vergelijkingen van oorsprong en schemeful same-site, ondersteund door de URL-parser en de Public Suffix List van de browser.
De juiste vergelijking voor uw scenario kiezen
- Gebruik
isSameOrigin()voor strikte rechtenlimieten: Vereis een exacte(scheme, host, port)-overeenkomst bij de verwerking vanpostMessage-opdrachten voor meerdere vensters (zoals een ingesloten betaal- of verificatiepop-up) of bij de beslissing of je inloggegevens wilt bijvoegen. Zelfs een ander subdomein (https://blog.example.comversushttps://app.example.com) is een andere oorsprong en moet worden geweigerd, zodat een kwetsbaarheid op het ene subdomein geen controle kan krijgen over een ander subdomein. - Gebruik
isSameSite()voor navigatie tussen subdomeinen en linkclassificatie: Sta elk subdomein of elke poort toe die hetzelfde schema en registreerbare domein deelt bij de validatie van omleidings-URL's na het inloggen (?returnTo=https://billing.example.co.uk/orders) of bij de classificatie van links als intern of extern.isSameSite()weigert zowel downgrades van hethttp:-schema als sibling-tenants op gedeelde openbare achtervoegsels (.co.uk,.github.io). - Gebruik
origin.opaqueenOrigin.from(event)voor ondoorzichtige of sandboxed contexten: Wijsdata:-URL's voor omleidingen (origin.opaque === true) af of pin de ondoorzichtigeOrigindie is geëxtraheerd viaOrigin.from(event)wanneer je communiceert met een specifieke<iframe sandbox="allow-scripts">-widget waarvan de geserialiseerdeevent.origin"null"is.
1. Exacte herkomsten verifiëren voor bevoorrechte acties (Origin.from() en isSameOrigin())
Gebruik Origin.from() om een Origin-object te extraheren en vergelijk het met een vertrouwde Origin met isSameOrigin():
- Beide kanten moeten
Origin-instanties zijn:isSameOrigin(other)enisSameSite(other)accepteren alleen eenOrigin-object. Als je een onbewerkte tekenreeks doorgeeft, wordt er eenTypeErrorgegenereerd. - Ondersteunde
Origin.from()-invoer:- Geserialiseerde URL-tekenreeksen (bijvoorbeeld
'https://app.example.com:443/path'). Elke ongeldige URL-tekenreeks, bijvoorbeeld'example'of de'null'-tekenreeks die doorevent.originis geserialiseerd voor ondoorzichtige oorsprongen, zorgt ervoor datOrigin.from()eenTypeErrorgenereert. - Platformobjecten die stappen voor oorsprongsextractie definiëren in de HTML-specificatie:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>), same-originWindowofWorkerGlobalScope(Origin.from(self)) en door de browser verzondenMessageEvent-instanties (Origin.from(event)). - Objecten zoals
Location,WorkerLocation,Document,RequestenResponsedefiniëren geen stappen voor het extraheren van de oorsprong en geven eenTypeErrorals ze rechtstreeks worden doorgegeven aanOrigin.from(). Geef in plaats daarvanself(voor de huidige context) ofrequest.url/response.urldoor.
- Geserialiseerde URL-tekenreeksen (bijvoorbeeld
- Verwerk zowel door de browser verzonden als synthetische
MessageEvent-instanties: Echte browsergebeurtenissen van het typepostMessagehebben een interne oorsprong die wordt geëxtraheerd doorOrigin.from(event), terwijl synthetische gebeurtenissen van het typenew MessageEvent('message', { origin })die zijn gemaakt in unittests alleen de tekenreeksevent.origininvullen en een uitzondering genereren voorOrigin.from(event). Als je terugvalt opOrigin.from(event.origin)in eentry...catch, worden beide netjes verwerkt, terwijl ongeldige of"null"-tekenreeksen nog steeds worden geweigerd.
// 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. Omleidingen en links tussen subdomeinen valideren (isSameSite())
Gebruik isSameSite() als je omleidingstargets (zoals ?returnTo=-parameters) of interne links tussen subdomeinen van hetzelfde registreerbare domein (bijvoorbeeld https://app.example.co.uk en https://billing.example.co.uk:8443) valideert:
isSameSite()voert een schemeful same-site-vergelijking uit:https://sub.example.comenhttp://sub.example.comzijn niet same-site omdat hun schema's verschillen.isSameSite()gebruikt de ingebouwde Public Suffix List van de browser, zodat verschillende tenants op gedeelde openbare achtervoegsels (zoalshttps://tenant-a.github.ioenhttps://tenant-b.github.io, ofhttps://a.co.ukenhttps://b.co.uk) worden geëvalueerd alsfalse.
// 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. Ondoorzichtige oorsprongen onderscheiden en vastzetten (origin.opaque en Origin.from(event))
Sandboxed iframes (<iframe sandbox="allow-scripts">), data:-URL's en new Origin() produceren ondoorzichtige oorsprongen (origin.opaque === true). Hoewel de tekenreeksserialisatie altijd "null" is, haalt Origin.from(event) de daadwerkelijke onderliggende ondoorzichtige oorsprong van de afzender op uit een door de browser verzonden MessageEvent:
- 2 berichten die vanuit dezelfde sandbox-iframe worden gestuurd, leveren
Origin-objecten op dieisSameOrigin()met elkaar zijn (true). - Berichten die worden gestuurd vanuit verschillende sandboxed iframes (of afzonderlijke
Origin.from('data:...')- ofnew Origin()-aanroepen) leveren verschillende ondoorzichtige herkomsten op dieisSameOrigin()evalueren naarfalse. Zo wordt"null" === "null"-spoofing in niet-gerelateerde ondoorzichtige contexten voorkomen. new Origin()maakt een nieuwe, unieke ondoorzichtige oorsprong die alleen overeenkomt met zichzelf. Dit is handig als standaard sentinel die alles weigert voordat een verwachte oorsprong van een peer eenmalig wordt vastgezet bij de initialisatie.
// 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;
}
}
Praktische tips
- WEL: Converteer beide kanten van een vergelijking naar
Origin-instanties viaOrigin.from()voordat uisSameOrigin(other)ofisSameSite(other)aanroept. - DO pass a browser-dispatched
MessageEventdirectly toOrigin.from(event)when you need to preserve opaque origin identity, and useOrigin.from(self)(rather thanLocationorDocument, which throwTypeError) to obtain the current execution context'sOrigin. - 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. - Vergelijk NIET 2 runtime-tekenreeksen van
event.originofurl.originrechtstreeks zonder eerst"null"(origin !== 'null') af te wijzen, omdat 2 niet-gerelateerde ondoorzichtige oorsprongen beide worden geserialiseerd naar"null". - Valideer GEEN herkomsten of sites met
startsWith(),includes(),endsWith()of naïeve.split('.')-hostnaam-slicing.
Reservestrategie
Als je Baseline-doel browsers bevat die de Origin-interface nog niet ondersteunen, voer je een functieherkenning uit voor 'Origin' in globalThis en val je terug op het parseren van URL's met new URL():
- Same-origin fallback (tuple origins): Extraheer de URL-tekenreeks (met
event.originvoorMessageEvent-objecten ofcandidate.hrefvoor linkelementen), parseer zowel de kandidaat- als de vertrouwde URL's metnew URL(), wijs"null"ondoorzichtige oorsprongen (candidateUrl.origin !== 'null') af en vergelijkcandidateUrl.origin === trustedUrl.origin. Omdat een ondoorzichtige oorsprong nooit dezelfde oorsprong heeft als een tuple-oorsprong zoals'https://app.example.com', komt onmiddellijke afwijzing van'null'overeen metOrigin.isSameOrigin(). - Same-site fallback (caveat): De
URL-interface toont de Public Suffix List van de browser niet. In browsers zonderOriginvoer je alleen achtervoegselovereenkomsten uit voor een expliciet, bekend privé registreerbaar domein dat je beheert (waarbij je zowelprotocolals een exacte of door een punt voorafgeganehostname-overeenkomst verifieert) of gebruik je een onderhouden bibliotheek met openbare achtervoegsels. - Ondoorzichtige/sandboxed iframe-fallback: Browsers zonder
Origintonen geen afzonderlijke ondoorzichtige oorsprongsidentiteiten opMessageEvent(event.originis altijd"null"). Als je opzettelijk berichten accepteert van een bekende sandboxed<iframe sandbox="allow-scripts">, verifieer jeevent.source === expectedWidgetWindow(of communiceer je via een specialeMessageChannelMessagePortdie bij de initialisatie naar het iframe is overgezet) in plaats vanevent.originte vergelijken.
// 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;
}
}