Embora a comparação de event.origin com um literal de string fixo e normalizado por porta (como event.origin === 'https://app.example.com') funcione para listas de permissões simples, as comparações manuais de strings e as expressões regulares falham ao validar URLs arbitrários, elementos de link do DOM, redirecionamentos entre subdomínios ou mensagens em sandbox:
- Spoofing de prefixo e substring: a verificação de
url.startsWith('https://app.example.com')é ignorada por'https://app.example.com.attacker.example'. - URLs não normalizados: URLs completos com caminhos, strings de consulta ou portas padrão explícitas (
'https://app.example.com:443/path') não correspondem a'https://app.example.com'em igualdade de string sem primeiro analisar e normalizar. - Erros na lista de sufixos públicos: a divisão de nomes de host em
.para verificar relações no mesmo site falha em sufixos públicos com vários rótulos (como.co.ukou domínios hospedados como.github.io) e ignora incompatibilidades de esquema (http:xhttps:). - Colisões de origem
"null"opacas: iframes em sandbox (<iframe sandbox="allow-scripts">) e URLsdata:serializamevent.originpara a string literal"null". Comparar duas origens de ambiente de execução serializadas comoriginA === originBiguala falsamente dois contextos opacos não relacionados ("null" === "null").
A API Origin (Origin.from(), isSameOrigin(), isSameSite() e opaque) oferece comparações estruturadas de origem e de mesmo site com esquema, em conformidade com as especificações, apoiadas pelo analisador de URL do navegador e pela lista de sufixos públicos.
Como escolher a comparação certa para seu cenário
- Use
isSameOrigin()para limites de privilégio estritos: exija uma correspondência exata de(scheme, host, port)ao processar comandospostMessageentre janelas (como um pop-up de finalização de compra ou autenticação incorporado) ou ao decidir se as credenciais serão anexadas. Até mesmo um subdomínio diferente (https://blog.example.comem vez dehttps://app.example.com) é uma origem diferente e precisa ser rejeitado para que uma vulnerabilidade em um subdomínio não controle outro. - Use
isSameSite()para navegação entre subdomínios e classificação de links: permita que qualquer subdomínio ou porta compartilhe o mesmo esquema e domínio registrável ao validar URLs de redirecionamento pós-login (?returnTo=https://billing.example.co.uk/orders) ou classificar links como internos ou externos. OisSameSite()rejeita downgrades do esquemahttp:e locatários irmãos em sufixos públicos compartilhados (.co.uk,.github.io). - Use
origin.opaqueeOrigin.from(event)para contextos opacos ou em sandbox: rejeite URLsdata:para redirecionamentos (origin.opaque === true) ou fixe oOriginopaco extraído viaOrigin.from(event)ao se comunicar com um widget<iframe sandbox="allow-scripts">específico cujoevent.originserializado é"null".
1. Verificar origens exatas para ações privilegiadas (Origin.from() e isSameOrigin())
Use Origin.from() para extrair um objeto Origin e compará-lo com um Origin confiável usando isSameOrigin():
- Os dois lados precisam ser instâncias de
Origin:isSameOrigin(other)eisSameSite(other)só aceitam um objetoOrigin. Transmitir uma string bruta gera umTypeError. - Entradas de
Origin.from()compatíveis:- Strings de URL serializadas (por exemplo,
'https://app.example.com:443/path'). Qualquer string de URL inválida, como'example'ou a string'null'serializada porevent.originpara origens opacas, faz com queOrigin.from()gere umTypeError. - Objetos de plataforma que definem etapas de extração de origem na especificação HTML:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>),WindowouWorkerGlobalScope(Origin.from(self)) de mesma origem e instâncias deMessageEvent(Origin.from(event)) enviadas pelo navegador. - Objetos como
Location,WorkerLocation,Document,RequesteResponsenão definem etapas de extração de origem e geram umTypeErrorse transmitidos diretamente paraOrigin.from(). Transmitaself(para o contexto atual) ourequest.url/response.url.
- Strings de URL serializadas (por exemplo,
- Processar instâncias
MessageEventsintéticas e enviadas pelo navegador: os eventospostMessagereais do navegador carregam uma origem interna extraída porOrigin.from(event), enquanto os eventosnew MessageEvent('message', { origin })sintéticos criados em testes de unidade preenchem apenas a stringevent.origine são gerados emOrigin.from(event). O uso deOrigin.from(event.origin)dentro de umtry...catchlida com ambos de forma limpa, rejeitando strings inválidas ou"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. Validar redirecionamentos e links entre subdomínios (isSameSite())
Ao validar destinos de redirecionamento (como parâmetros ?returnTo=) ou links internos em subdomínios do mesmo domínio registrável (por exemplo, https://app.example.co.uk e https://billing.example.co.uk:8443), use isSameSite():
isSameSite()realiza uma comparação de mesmo site com esquema:https://sub.example.comehttp://sub.example.comnão são o mesmo site porque os esquemas são diferentes.- O
isSameSite()usa a lista de sufixos públicos integrada do navegador. Assim, locatários distintos em sufixos públicos compartilhados (comohttps://tenant-a.github.ioehttps://tenant-b.github.ioouhttps://a.co.ukehttps://b.co.uk) são avaliados comofalse.
// 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. Distinguir e fixar origens opacas (origin.opaque e Origin.from(event))
Iframes em sandbox (<iframe sandbox="allow-scripts">), URLs data: e new Origin() produzem origens opacas (origin.opaque === true). Embora a serialização de string seja sempre "null", Origin.from(event) extrai a origem opaca real do remetente de um MessageEvent enviado pelo navegador:
- Duas mensagens enviadas do mesmo iframe em sandbox geram objetos
Originque sãoisSameOrigin()entre si (true). - As mensagens enviadas de iframes em sandbox diferentes ou chamadas
Origin.from('data:...')/new Origin()separadas geram origens opacas distintas que avaliamisSameOrigin()comofalse, impedindo a falsificação de"null" === "null"em contextos opacos não relacionados. new Origin()cria uma origem opaca nova e exclusiva que não corresponde a nada, exceto a si mesma, o que é útil como uma sentinela padrão de negação total antes que uma origem de peering esperada seja fixada uma vez na inicialização.
// 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;
}
}
Práticas recomendadas
- RECOMENDAMOS converter os dois lados de uma comparação em instâncias
OriginusandoOrigin.from()antes de chamarisSameOrigin(other)ouisSameSite(other). - É RECOMENDÁVEL transmitir um
MessageEventenviado pelo navegador diretamente paraOrigin.from(event)quando você precisar preservar a identidade de origem opaca e usarOrigin.from(self)(em vez deLocationouDocument, que geramTypeError) para receber oOrigindo contexto de execução atual. - FAÇA o encapsulamento de chamadas
Origin.from()em entradas não confiáveis ou de ambiente de execução em um blocotry...catchpara que URLs inválidos, strings"null"ou objetos não compatíveis retornemfalsecom segurança em vez de gerar umTypeErrornão capturado. - NÃO compare duas strings de ambiente de execução
event.originouurl.origindiretamente sem antes rejeitar"null"(origin !== 'null'), já que duas origens opacas não relacionadas são serializadas para"null". - NÃO valide origens ou sites com
startsWith(),includes(),endsWith()ou fatiamento simples de nome do host.split('.').
Estratégia de fallback
Se o destino do Baseline incluir navegadores que ainda não oferecem suporte à interface Origin, use a detecção de recursos para 'Origin' in globalThis e use new URL() como substituto para analisar URLs:
- Substituição de mesma origem (origens de tupla): extraia a string de URL (usando
event.originpara objetosMessageEventoucandidate.hrefpara elementos de link), analise os URLs candidatos e confiáveis comnew URL(), rejeite origens opacas"null"(candidateUrl.origin !== 'null') e comparecandidateUrl.origin === trustedUrl.origin. Como uma origem opaca nunca é de mesma origem com uma origem de tupla como'https://app.example.com', rejeitar imediatamente'null'corresponde aOrigin.isSameOrigin(). - Substituto do mesmo site (ressalva): a interface
URLnão expõe a lista de sufixos públicos do navegador. Em navegadores semOrigin, faça a correspondência de sufixo apenas com um domínio registrável particular explícito e conhecido que você controla (verificandoprotocole uma correspondência exata ou com prefixo de pontohostname) ou use uma biblioteca de lista de sufixos públicos mantida. - Fallback de iframe opaco / em sandbox: navegadores sem
Originnão expõem identidades de origem opacas distintas emMessageEvent(event.originé sempre"null"). Se você aceitar intencionalmente mensagens de um<iframe sandbox="allow-scripts">em sandbox conhecido, verifiqueevent.source === expectedWidgetWindow(ou se comunique por umMessageChannelMessagePortdedicado transferido para o iframe na inicialização) em vez de compararevent.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;
}
}