Web-oorsprongen en sites vergelijken en valideren

Webfuncties: Oorsprong

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.uk of gedeelde hostingdomeinen zoals .github.io) en negeert niet-overeenkomende schema's (http: versus https:).
  • Ondoorzichtige "null"-oorsprongbotsingen: Sandboxed iframes (<iframe sandbox="allow-scripts">) en data:-URL's serialiseren event.origin naar de letterlijke tekenreeks "null". Als je 2 geserialiseerde runtime-oorsprongen vergelijkt met originA === 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 van postMessage-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.com versus https://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 het http:-schema als sibling-tenants op gedeelde openbare achtervoegsels (.co.uk, .github.io).
  • Gebruik origin.opaque en Origin.from(event) voor ondoorzichtige of sandboxed contexten: Wijs data:-URL's voor omleidingen (origin.opaque === true) af of pin de ondoorzichtige Origin die is geëxtraheerd via Origin.from(event) wanneer je communiceert met een specifieke <iframe sandbox="allow-scripts">-widget waarvan de geserialiseerde event.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) en isSameSite(other) accepteren alleen een Origin-object. Als je een onbewerkte tekenreeks doorgeeft, wordt er een TypeError gegenereerd.
  • 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 door event.origin is geserialiseerd voor ondoorzichtige oorsprongen, zorgt ervoor dat Origin.from() een TypeError genereert.
    • Platformobjecten die stappen voor oorsprongsextractie definiëren in de HTML-specificatie: Origin, URL, HTMLAnchorElement (<a>), HTMLAreaElement (<area>), same-origin Window of WorkerGlobalScope (Origin.from(self)) en door de browser verzonden MessageEvent-instanties (Origin.from(event)).
    • Objecten zoals Location, WorkerLocation, Document, Request en Response definiëren geen stappen voor het extraheren van de oorsprong en geven een TypeError als ze rechtstreeks worden doorgegeven aan Origin.from(). Geef in plaats daarvan self (voor de huidige context) of request.url / response.url door.
  • Verwerk zowel door de browser verzonden als synthetische MessageEvent-instanties: Echte browsergebeurtenissen van het type postMessage hebben een interne oorsprong die wordt geëxtraheerd door Origin.from(event), terwijl synthetische gebeurtenissen van het type new MessageEvent('message', { origin }) die zijn gemaakt in unittests alleen de tekenreeks event.origin invullen en een uitzondering genereren voor Origin.from(event). Als je terugvalt op Origin.from(event.origin) in een try...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
});

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.com en http://sub.example.com zijn 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 (zoals https://tenant-a.github.io en https://tenant-b.github.io, of https://a.co.uk en https://b.co.uk) worden geëvalueerd als 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. 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 die isSameOrigin() met elkaar zijn (true).
  • Berichten die worden gestuurd vanuit verschillende sandboxed iframes (of afzonderlijke Origin.from('data:...')- of new Origin()-aanroepen) leveren verschillende ondoorzichtige herkomsten op die isSameOrigin() evalueren naar false. 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 via Origin.from() voordat u isSameOrigin(other) of isSameSite(other) aanroept.
  • DO pass a browser-dispatched MessageEvent directly to Origin.from(event) when you need to preserve opaque origin identity, and use Origin.from(self) (rather than Location or Document, which throw TypeError) to obtain the current execution context's Origin.
  • DO wrap Origin.from() calls on untrusted or runtime inputs in a try...catch block so invalid URLs, "null" strings, or unsupported objects safely return false instead of throwing an uncaught TypeError.
  • Vergelijk NIET 2 runtime-tekenreeksen van event.origin of url.origin rechtstreeks 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.origin voor MessageEvent-objecten of candidate.href voor linkelementen), parseer zowel de kandidaat- als de vertrouwde URL's met new URL(), wijs "null" ondoorzichtige oorsprongen (candidateUrl.origin !== 'null') af en vergelijk candidateUrl.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 met Origin.isSameOrigin().
  • Same-site fallback (caveat): De URL-interface toont de Public Suffix List van de browser niet. In browsers zonder Origin voer je alleen achtervoegselovereenkomsten uit voor een expliciet, bekend privé registreerbaar domein dat je beheert (waarbij je zowel protocol als een exacte of door een punt voorafgegane hostname-overeenkomst verifieert) of gebruik je een onderhouden bibliotheek met openbare achtervoegsels.
  • Ondoorzichtige/sandboxed iframe-fallback: Browsers zonder Origin tonen geen afzonderlijke ondoorzichtige oorsprongsidentiteiten op MessageEvent (event.origin is altijd "null"). Als je opzettelijk berichten accepteert van een bekende sandboxed <iframe sandbox="allow-scripts">, verifieer je event.source === expectedWidgetWindow (of communiceer je via een speciale MessageChannel MessagePort die bij de initialisatie naar het iframe is overgezet) in plaats van event.origin te 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;
  }
}