Sebbene il confronto di event.origin con un valore letterale stringa fisso e normalizzato per la porta (come event.origin === 'https://app.example.com') funzioni per le liste consentite semplici, i confronti manuali delle stringhe e le espressioni regolari non funzionano quando si convalidano URL arbitrari, elementi link DOM, reindirizzamenti tra sottodomini o messaggi in sandbox:
- Spoofing di prefissi e sottostringhe: il controllo di
url.startsWith('https://app.example.com')viene ignorato da'https://app.example.com.attacker.example'. - URL non normalizzati: gli URL completi con percorsi, stringhe di query o porte predefinite esplicite (
'https://app.example.com:443/path') non corrispondono a'https://app.example.com'in base all'uguaglianza delle stringhe senza prima eseguire l'analisi e la normalizzazione. - Errori nell'elenco dei suffissi pubblici: la suddivisione dei nomi host su
.per verificare le relazioni SameSite non riesce sui suffissi pubblici con più etichette (come.co.uko domini ospitati come.github.io) e ignora le mancate corrispondenze degli schemi (http:rispetto ahttps:). - Collisioni di origini opache
"null": gli iframe con sandbox (<iframe sandbox="allow-scripts">) e gli URLdata:serializzanoevent.originnella stringa letterale"null". Il confronto di due origini di runtime serializzate conoriginA === originBequipara erroneamente due contesti opachi non correlati ("null" === "null").
L'API Origin (Origin.from(), isSameOrigin(), isSameSite() e opaque) fornisce confronti strutturati e conformi alle specifiche di origine e schemeful same-site supportati dal parser URL del browser e dall'elenco dei suffissi pubblici.
Scegliere il confronto giusto per il tuo scenario
- Utilizzo
isSameOrigin()per limiti di privilegio rigorosi: Richiedi una(scheme, host, port)corrispondenza esatta durante la gestione deipostMessagecomandi tra finestre (come un popup di pagamento o di autenticazione incorporato) o decidere se allegare le credenziali. Anche un sottodominio diverso (https://blog.example.comrispetto ahttps://app.example.com) è un'origine diversa e deve essere rifiutato in modo che una vulnerabilità in un sottodominio non possa controllarne un altro. - Utilizza
isSameSite()per la navigazione tra sottodomini e la classificazione dei link: consenti qualsiasi sottodominio o porta che condivida lo stesso schema e dominio registrabile durante la convalida degli URL di reindirizzamento post-accesso (?returnTo=https://billing.example.co.uk/orders) o la classificazione dei link come interni o esterni.isSameSite()rifiuta sia i downgrade dello schemahttp:sia i tenant di pari livello su suffissi pubblici condivisi (.co.uk,.github.io). - Utilizza
origin.opaqueeOrigin.from(event)per contesti opachi o in sandbox: rifiuta gli URLdata:per i reindirizzamenti (origin.opaque === true) o fissa l'Originopaco estratto tramiteOrigin.from(event)quando comunichi con un widget<iframe sandbox="allow-scripts">specifico la cuievent.originserializzata è"null".
1. Verifica le origini esatte per le azioni con privilegi (Origin.from() e isSameOrigin())
Utilizza Origin.from() per estrarre un oggetto Origin e confrontarlo con un Origin attendibile utilizzando isSameOrigin():
- Entrambi i lati devono essere istanze
Origin:isSameOrigin(other)eisSameSite(other)accettano solo un oggettoOrigin; il passaggio di una stringa non elaborata genera unTypeError. - Input
Origin.from()supportati:- Stringhe URL serializzate (ad esempio,
'https://app.example.com:443/path'). Tieni presente che qualsiasi stringa URL non valida, ad esempio'example'o la stringa'null'serializzata daevent.originper le origini opache, fa sì cheOrigin.from()generi unTypeError. - Oggetti della piattaforma che definiscono i passaggi di estrazione dell'origine nella specifica HTML:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>),WindowoWorkerGlobalScope(Origin.from(self)) della stessa origine e istanzeMessageEvent(Origin.from(event)) inviate dal browser. - Oggetti come
Location,WorkerLocation,Document,RequesteResponsenon definiscono i passaggi di estrazione dell'origine e generano unTypeErrorse passati direttamente aOrigin.from(). Passa inveceself(per il contesto corrente) orequest.url/response.url.
- Stringhe URL serializzate (ad esempio,
- Gestisci le istanze
MessageEventinviate dal browser e sintetiche: gli eventipostMessagedel browser reale contengono un'origine interna estratta daOrigin.from(event), mentre gli eventinew MessageEvent('message', { origin })sintetici creati nei test di unità compilano solo la stringaevent.origine generano un errore suOrigin.from(event). Il fallback aOrigin.from(event.origin)all'interno di untry...catchgestisce entrambi in modo pulito, rifiutando comunque le stringhe non valide o"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. Convalida i reindirizzamenti e i link tra sottodomini (isSameSite())
Quando convalidi i target di reindirizzamento (come i parametri ?returnTo=) o i link interni tra i sottodomini dello stesso dominio registrabile (ad esempio https://app.example.co.uk e https://billing.example.co.uk:8443), utilizza isSameSite():
isSameSite()esegue un confronto schemeful same-site:https://sub.example.comehttp://sub.example.comnon sono same-site perché i loro schemi sono diversi.isSameSite()utilizza l'elenco dei suffissi pubblici integrato nel browser, pertanto i tenant distinti su suffissi pubblici condivisi (comehttps://tenant-a.github.ioehttps://tenant-b.github.ioohttps://a.co.ukehttps://b.co.uk) vengono valutati comefalse.
// 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. Distinguere e bloccare le origini opache (origin.opaque e Origin.from(event))
Gli iframe con sandbox (<iframe sandbox="allow-scripts">), gli URL data: e new Origin() producono origini opache (origin.opaque === true). Sebbene la loro serializzazione delle stringhe sia sempre "null", Origin.from(event) estrae l'origine opaca sottostante effettiva del mittente da un MessageEvent inviato dal browser:
- Due messaggi inviati dallo stesso iframe con sandbox producono oggetti
Originche sonoisSameOrigin()tra loro (true). - I messaggi inviati da iframe con sandbox diversi o chiamate
Origin.from('data:...')/new Origin()separate generano origini opache distinte che valutanoisSameOrigin()comefalse, impedendo lo spoofing di"null" === "null"in contesti opachi non correlati. new Origin()crea un'origine opaca nuova e univoca che non corrisponde a nient'altro se non a se stessa, il che è utile come sentinella predefinita di negazione di tutto prima che un'origine peer prevista venga fissata una volta durante l'inizializzazione.
// 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 practice
- SÌ: converti entrambi i lati di un confronto in istanze
OrigintramiteOrigin.from()prima di chiamareisSameOrigin(other)oisSameSite(other). - DEVI passare un
MessageEventinviato dal browser direttamente aOrigin.from(event)quando devi preservare l'identità dell'origine opaca e utilizzareOrigin.from(self)(anzichéLocationoDocument, che generanoTypeError) per ottenere ilOrigindel contesto di esecuzione corrente. - SÌ: racchiudi le chiamate
Origin.from()su input non attendibili o di runtime in un bloccotry...catchin modo che URL non validi, stringhe"null"o oggetti non supportati restituiscano in sicurezzafalseanziché generare unTypeErrornon rilevato. - NON confrontare direttamente due stringhe di runtime
event.originourl.originsenza prima rifiutare"null"(origin !== 'null'), poiché due origini opache non correlate vengono entrambe serializzate in"null". - NON convalidare origini o siti con
startsWith(),includes(),endsWith()o slicing ingenuo del nome host.split('.').
Strategia di riserva
Se il tuo target Baseline include browser che non supportano ancora l'interfaccia Origin, esegui il rilevamento delle funzionalità di 'Origin' in globalThis e utilizza il fallback per l'analisi degli URL con new URL():
- Fallback della stessa origine (origini delle tuple): estrai la stringa dell'URL (utilizzando
event.originper gli oggettiMessageEventocandidate.hrefper gli elementi link), analizza gli URL candidati e attendibili connew URL(), rifiuta le origini opache"null"(candidateUrl.origin !== 'null') e confrontacandidateUrl.origin === trustedUrl.origin. Poiché un'origine opaca non è mai della stessa origine di un'origine tupla come'https://app.example.com', il rifiuto immediato di'null'corrisponde aOrigin.isSameOrigin(). - Fallback per lo stesso sito (avviso): l'interfaccia
URLnon espone l'elenco dei suffissi pubblici del browser. Nei browser senzaOrigin, esegui la corrispondenza del suffisso solo con un dominio registrabile privato esplicito e noto che controlli (verificando siaprotocolsia una corrispondenzahostnameesatta o con prefisso punto) oppure utilizza una libreria Public Suffix List gestita. - Fallback iframe opaco/sandbox: i browser senza
Originnon espongono identità di origine opache distinte suMessageEvent(event.originè sempre"null"). Se accetti intenzionalmente messaggi da un<iframe sandbox="allow-scripts">con sandbox noto, verificaevent.source === expectedWidgetWindow(o comunica tramite unMessagePortMessageChanneldedicato trasferito all'iframe durante l'inizializzazione) anziché confrontareevent.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;
}
}