Bien que la comparaison de event.origin avec un littéral de chaîne fixe normalisé par port (tel que event.origin === 'https://app.example.com') fonctionne pour les listes d'autorisation simples, les comparaisons de chaînes manuelles et les expressions régulières ne fonctionnent pas lors de la validation d'URL arbitraires, d'éléments link DOM, de redirections entre sous-domaines ou de messages en bac à sable :
- Spoofing de préfixe et de sous-chaîne : la vérification de
url.startsWith('https://app.example.com')est contournée par'https://app.example.com.attacker.example'. - URL non normalisées : les URL complètes avec des chemins d'accès, des chaînes de requête ou des ports par défaut explicites (
'https://app.example.com:443/path') ne correspondent pas à'https://app.example.com'en cas d'égalité de chaîne sans analyse ni normalisation préalables. - Erreurs de la liste des suffixes publics : la division des noms d'hôte sur
.pour vérifier les relations SameSite échoue sur les suffixes publics à plusieurs libellés (comme.co.ukou les domaines d'hébergement partagé comme.github.io) et ignore les incohérences de schéma (http:par rapport àhttps:). - Collisions d'origine
"null"opaque : les iFrames en bac à sable (<iframe sandbox="allow-scripts">) et les URLdata:sérialisentevent.originen chaîne littérale"null". La comparaison de deux origines d'exécution sérialisées avecoriginA === originBassimile à tort deux contextes opaques non liés ("null" === "null").
L'API Origin (Origin.from(), isSameOrigin(), isSameSite() et opaque) fournit des comparaisons d'origine et de même site structurées et conformes aux spécifications, basées sur l'analyseur d'URL du navigateur et la liste des suffixes publics.
Choisir la comparaison adaptée à votre scénario
- Utilisez
isSameOrigin()pour définir des limites de privilèges strictes : exigez une correspondance exacte de(scheme, host, port)lors du traitement des commandespostMessageentre fenêtres (comme un pop-up de paiement ou d'authentification intégré) ou pour décider d'associer ou non des identifiants. Même un sous-domaine différent (https://blog.example.compar rapport àhttps://app.example.com) est une origine différente et doit être rejeté afin qu'une faille sur un sous-domaine ne puisse pas en contrôler un autre. - Utilisez
isSameSite()pour la navigation entre sous-domaines et la classification des liens : autorisez tout sous-domaine ou port partageant le même schéma et le même domaine enregistrable lors de la validation des URL de redirection après connexion (?returnTo=https://billing.example.co.uk/orders) ou de la classification des liens comme internes ou externes.isSameSite()refuse à la fois les rétrogradations de schémahttp:et les locataires frères sur les suffixes publics partagés (.co.uk,.github.io). - Utilisez
origin.opaqueetOrigin.from(event)pour les contextes opaques ou de bac à sable : refusez les URLdata:pour les redirections (origin.opaque === true) ou épinglez l'Originopaque extrait viaOrigin.from(event)lorsque vous communiquez avec un widget<iframe sandbox="allow-scripts">spécifique dont l'event.originsérialisé est"null".
1. Vérifier les origines exactes des actions privilégiées (Origin.from() et isSameOrigin())
Utilisez Origin.from() pour extraire un objet Origin et le comparer à un Origin de confiance à l'aide de isSameOrigin() :
- Les deux côtés doivent être des instances
Origin:isSameOrigin(other)etisSameSite(other)n'acceptent qu'un objetOrigin. Transmettre une chaîne brute génère uneTypeError. - Entrées
Origin.from()compatibles :- Chaînes d'URL sérialisées (par exemple,
'https://app.example.com:443/path'). Notez que toute chaîne d'URL non valide (par exemple,'example'ou la chaîne'null'sérialisée parevent.originpour les origines opaques) entraîne l'affichage d'uneTypeErrorparOrigin.from(). - Objets de plate-forme qui définissent les étapes d'extraction de l'origine dans la spécification HTML :
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>),WindowouWorkerGlobalScopede même origine (Origin.from(self)) et instancesMessageEventenvoyées par le navigateur (Origin.from(event)). - Les objets tels que
Location,WorkerLocation,Document,RequestetResponsene définissent pas d'étapes d'extraction d'origine et génèrent uneTypeErrors'ils sont transmis directement àOrigin.from(). Transmettez plutôtself(pour le contexte actuel) ourequest.url/response.url.
- Chaînes d'URL sérialisées (par exemple,
- Gérer les instances
MessageEventenvoyées par le navigateur et synthétiques : les événementspostMessageréels du navigateur comportent une origine interne extraite parOrigin.from(event), tandis que les événementsnew MessageEvent('message', { origin })synthétiques créés dans les tests unitaires ne renseignent que la chaîneevent.originet génèrent une exception surOrigin.from(event). Le recours àOrigin.from(event.origin)dans untry...catchgère les deux de manière propre, tout en rejetant les chaînes non valides 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. Valider les redirections et les liens entre sous-domaines (isSameSite())
Lorsque vous validez des cibles de redirection (comme les paramètres ?returnTo=) ou des liens internes entre des sous-domaines d'un même domaine enregistrable (par exemple, https://app.example.co.uk et https://billing.example.co.uk:8443), utilisez isSameSite() :
isSameSite()effectue une comparaison Schemeful Same-Site :https://sub.example.comethttp://sub.example.comne sont pas du même site, car leurs schémas diffèrent.isSameSite()utilise la liste des suffixes publics intégrée au navigateur. Ainsi, les locataires distincts sur des suffixes publics partagés (tels quehttps://tenant-a.github.ioethttps://tenant-b.github.io, ouhttps://a.co.ukethttps://b.co.uk) sont évalués à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. Distinction et épinglage des origines opaques (origin.opaque et Origin.from(event))
Les iFrames en bac à sable (<iframe sandbox="allow-scripts">), les URL data: et new Origin() produisent des origines opaques (origin.opaque === true). Bien que leur sérialisation de chaîne soit toujours "null", Origin.from(event) extrait l'origine opaque sous-jacente réelle de l'expéditeur à partir d'un MessageEvent envoyé par le navigateur :
- Deux messages envoyés depuis la même iframe en bac à sable génèrent des objets
Originqui sontisSameOrigin()l'un avec l'autre (true). - Les messages envoyés à partir d'iframes en bac à sable différentes (ou d'appels
Origin.from('data:...')/new Origin()distincts) génèrent des origines opaques distinctes qui évaluentisSameOrigin()àfalse, ce qui empêche l'usurpation de"null" === "null"dans des contextes opaques non liés. new Origin()crée une origine opaque unique qui ne correspond à rien d'autre qu'elle-même. Cela est utile en tant que sentinelle par défaut de refus de tout avant qu'une origine de pair attendue ne soit épinglée une fois lors de l'initialisation.
// 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;
}
}
Bonnes pratiques
- À FAIRE : Convertissez les deux côtés d'une comparaison en instances
OriginviaOrigin.from()avant d'appelerisSameOrigin(other)ouisSameSite(other). - À FAIRE : transmettez un
MessageEventenvoyé par le navigateur directement àOrigin.from(event)lorsque vous devez préserver l'identité d'origine opaque, et utilisezOrigin.from(self)(plutôt queLocationouDocument, qui génèrentTypeError) pour obtenir leOrigindu contexte d'exécution actuel. - À FAIRE : encapsulez les appels
Origin.from()sur des entrées non fiables ou d'exécution dans un bloctry...catchafin que les URL non valides, les chaînes"null"ou les objets non compatibles renvoientfalsede manière sécurisée au lieu de générer uneTypeErrornon interceptée. - NE COMPAREZ PAS directement deux chaînes d'exécution
event.originouurl.originsans d'abord rejeter"null"(origin !== 'null'), car deux origines opaques non liées sont toutes deux sérialisées en"null". - NE validez PAS les origines ou les sites avec
startsWith(),includes(),endsWith()ou un découpage naïf du nom d'hôte.split('.').
Stratégie de remplacement
Si votre cible Baseline inclut des navigateurs qui ne sont pas encore compatibles avec l'interface Origin, détectez la fonctionnalité 'Origin' in globalThis et revenez à l'analyse des URL avec new URL() :
- Solution de secours de même origine (origines de tuple) : extrayez la chaîne d'URL (à l'aide de
event.originpour les objetsMessageEventou decandidate.hrefpour les éléments link), analysez les URL candidates et de confiance avecnew URL(), rejetez les origines opaques"null"(candidateUrl.origin !== 'null') et comparezcandidateUrl.origin === trustedUrl.origin. Étant donné qu'une origine opaque n'est jamais de même origine qu'une origine de tuple comme'https://app.example.com', le rejet immédiat de'null'correspond àOrigin.isSameOrigin(). - Solution de secours SameSite (mise en garde) : l'interface
URLn'expose pas la liste de suffixes publics du navigateur. Dans les navigateurs sansOrigin, n'effectuez la mise en correspondance des suffixes qu'avec un domaine enregistrable privé explicite et connu que vous contrôlez (en vérifiant à la foisprotocolet une correspondance exacte ou précédée d'un pointhostname), ou utilisez une bibliothèque de liste de suffixes publics gérée. - Solution de secours pour les iFrames opaques/dans un bac à sable : les navigateurs sans
Originn'exposent pas d'identités d'origine opaques distinctes surMessageEvent(event.originest toujours"null"). Si vous acceptez intentionnellement les messages d'un<iframe sandbox="allow-scripts">connu dans un bac à sable, vérifiezevent.source === expectedWidgetWindow(ou communiquez via unMessageChannelMessagePortdédié transféré à l'iFrame lors de l'initialisation) plutôt que de comparerevent.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;
}
}