Web kaynaklarını ve sitelerini karşılaştırma ve doğrulama

Web özellikleri: Kaynak

event.origin, sabit ve bağlantı noktası normalleştirilmiş bir dize değişmeziyle (ör. event.origin === 'https://app.example.com') karşılaştırıldığında basit izin verilenler listeleri için işe yarar. Ancak rastgele URL'ler, DOM bağlantı öğeleri, alt alan adları arası yönlendirmeler veya korumalı alana alınmış mesajlar doğrulanırken manuel dize karşılaştırmaları ve normal ifadeler başarısız olur:

  • Önek ve alt dize sahtekarlığı: url.startsWith('https://app.example.com') kontrolü 'https://app.example.com.attacker.example' tarafından atlanır.
  • Normalleştirilmemiş URL'ler: Yollar, sorgu dizeleri veya açık varsayılan bağlantı noktaları ('https://app.example.com:443/path') içeren tam URL'ler, önce ayrıştırılıp normalleştirilmeden dize eşitliği kapsamında 'https://app.example.com' ile eşleşmez.
  • Herkese Açık Sonek Listesi hataları: Aynı site ilişkilerini kontrol etmek için ana makine adlarını . ile bölmek, çok etiketli herkese açık soneklerde (ör. .co.uk veya .github.io gibi paylaşılan barındırma alan adları) başarısız olur ve şema uyuşmazlıklarını (http: ve https:) yoksayar.
  • Opak "null" kaynak çakışmaları: Korumalı alana alınmış iFrame'ler (<iframe sandbox="allow-scripts">) ve data: URL'ler, event.origin dizesini "null" dizesine dönüştürür. originA === originB ile iki serileştirilmiş çalışma zamanı kaynağını karşılaştırmak, iki alakasız opak bağlamı ("null" === "null") yanlış bir şekilde eşitler.

Origin API'si (Origin.from(), isSameOrigin(), isSameSite() ve opaque), tarayıcının URL ayrıştırıcısı ve Public Suffix List tarafından desteklenen, yapılandırılmış, spesifikasyonlara uygun kaynak ve şemalı aynı site karşılaştırmaları sağlar.

Senaryonuz için doğru karşılaştırmayı seçme

  • Katı ayrıcalık sınırları için isSameOrigin() kullanın: Pencereler arası postMessage komutlarını (ör. yerleşik ödeme veya kimlik doğrulama pop-up'ı) işlerken ya da kimlik bilgilerinin eklenip eklenmeyeceğine karar verirken tam (scheme, host, port) eşleşmesi zorunlu kılın. Farklı bir alt alan adı bile (https://blog.example.com ve https://app.example.com) farklı bir kaynaktır ve reddedilmelidir. Böylece bir alt alan adındaki güvenlik açığı diğerini kontrol edemez.
  • Alt alan adları arası gezinme ve bağlantı sınıflandırması için isSameSite() kullanın: Giriş sonrası yönlendirme URL'lerini (?returnTo=https://billing.example.co.uk/orders) doğrularken veya bağlantıları dahili ya da harici olarak sınıflandırırken aynı şemayı ve kaydedilebilir alan adını paylaşan tüm alt alan adlarına veya bağlantı noktalarına izin verin. isSameSite(), hem http: şema sürüm düşürmelerini hem de paylaşılan herkese açık son eklerdeki (.co.uk, .github.io) eş kiracıları reddeder.
  • Opak veya korumalı alan bağlamları için origin.opaque ve Origin.from(event) kullanın: Yönlendirmeler (origin.opaque === true) için data: URL'lerini reddedin veya serileştirilmiş event.origin değeri "null" olan belirli bir <iframe sandbox="allow-scripts"> widget'ıyla iletişim kurarken Origin.from(event) aracılığıyla ayıklanan opak Origin değerini sabitleyin.

1. Ayrıcalıklı İşlemler İçin Tam Kaynakları Doğrulama (Origin.from() ve isSameOrigin())

Bir Origin nesnesini ayıklamak ve isSameOrigin() kullanarak güvenilir bir Origin ile karşılaştırmak için Origin.from() kullanın:

  • Her iki taraf da Origin örneği olmalıdır: isSameOrigin(other) ve isSameSite(other) yalnızca bir Origin nesnesi kabul eder. Ham dize iletildiğinde TypeError hatası oluşur.
  • Desteklenen Origin.from() girişleri:
    • Serileştirilmiş URL dizeleri (ör. 'https://app.example.com:443/path'). Geçersiz URL dizelerinin (ör. 'example' veya opak kaynaklar için event.origin tarafından serileştirilen 'null' dizesi) Origin.from() yönteminin TypeError hatası vermesine neden olacağını unutmayın.
    • HTML spesifikasyonunda kaynak ayıklama adımlarını tanımlayan platform nesneleri: Origin, URL, HTMLAnchorElement (<a>), HTMLAreaElement (<area>), aynı kaynak Window veya WorkerGlobalScope (Origin.from(self)) ve tarayıcı tarafından gönderilen MessageEvent örnekleri (Origin.from(event)).
    • Location, WorkerLocation, Document, Request ve Response gibi nesneler, kaynak ayıklama adımlarını tanımlamaz ve doğrudan Origin.from() yöntemine iletilirse TypeError hatası verir. Bunun yerine self (mevcut bağlam için) veya request.url / response.url yöntemini iletin.
  • Hem tarayıcı tarafından gönderilen hem de yapay MessageEvent örneklerini işleyin: Gerçek tarayıcı postMessage etkinlikleri, Origin.from(event) tarafından ayıklanan dahili bir kaynak taşır. Birim testlerinde oluşturulan yapay new MessageEvent('message', { origin }) etkinlikleri ise yalnızca event.origin dizesini doldurur ve Origin.from(event) üzerinde hata verir. try...catch içinde Origin.from(event.origin) kullanmak, geçersiz veya "null" dizelerini reddederken her ikisini de düzgün bir şekilde işler.
// 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
});

Yönlendirme hedeflerini (ör. ?returnTo= parametreleri) veya aynı kayıtlı alanın alt alan adlarındaki (ör. https://app.example.co.uk ve https://billing.example.co.uk:8443) dahili bağlantıları doğrularken isSameSite() kullanın:

  • isSameSite(), şemalı aynı site karşılaştırması yapar: https://sub.example.com ve http://sub.example.com, şemaları farklı olduğu için aynı site değildir.
  • isSameSite(), tarayıcının yerleşik Herkese Açık Sonek Listesi'ni kullanır. Bu nedenle, paylaşılan herkese açık soneklerdeki farklı kiracılar (ör. https://tenant-a.github.io ve https://tenant-b.github.io veya https://a.co.uk ve https://b.co.uk) false olarak değerlendirilir.
// 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. Opak Kaynakları Ayırt Etme ve Sabitleme (origin.opaque ve Origin.from(event))

Korumalı alana alınmış iframe'ler (<iframe sandbox="allow-scripts">), data: URL'leri ve new Origin(), opak kaynaklar (origin.opaque === true) oluşturur. Dize serileştirmeleri her zaman "null" olsa da Origin.from(event), gönderenin gerçek temel opak kaynağını tarayıcı tarafından gönderilen bir MessageEvent öğesinden ayıklar:

  • Aynı korumalı alana alınmış iFrame'den gönderilen iki mesaj, birbiriyle isSameOrigin() olan Origin nesneleri (true) verir.
  • Farklı korumalı alan iframe'lerinden veya ayrı Origin.from('data:...') / new Origin() çağrılarından gönderilen mesajlar, isSameOrigin() değerini false olarak değerlendiren farklı opak kaynaklar oluşturur. Bu sayede, alakasız opak bağlamlarda "null" === "null" sahtekarlığı önlenir.
  • new Origin(), kendisi dışında hiçbir şeyle eşleşmeyen yeni ve benzersiz bir opak kaynak oluşturur. Bu, beklenen bir eş kaynağın başlatma sırasında bir kez sabitlenmesinden önce varsayılan olarak tümünü reddetme nöbetçisi olarak kullanışlıdır.
// 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;
  }
}

En iyi uygulamalar

  • isSameOrigin(other) veya isSameSite(other) yöntemini çağırmadan önce karşılaştırmanın her iki tarafını da Origin.from() aracılığıyla Origin örneklerine dönüştürün.
  • Opak kaynak kimliğini korumanız gerektiğinde tarayıcı tarafından gönderilen MessageEvent değerini doğrudan Origin.from(event) değerine GEÇİRİN ve geçerli yürütme bağlamının Origin değerini elde etmek için Origin.from(self) (TypeError hatası veren Location veya Document yerine) kullanın.
  • Güvenilmeyen veya çalışma zamanı girişlerindeki Origin.from() çağrılarını bir try...catch bloğuna sarmalayın. Böylece geçersiz URL'ler, "null" dizeleri veya desteklenmeyen nesneler, yakalanmamış bir TypeError hatası vermek yerine güvenli bir şekilde false döndürür.
  • DO NOT compare two runtime event.origin or url.origin strings directly without first rejecting "null" (origin !== 'null'), as two unrelated opaque origins both serialize to "null".
  • startsWith(), includes(), endsWith() veya basit .split('.') ana makine adı dilimleme ile kaynakları veya siteleri DOĞRULAMAYIN.

Yedek strateji

Baseline hedefiniz henüz Origin arayüzünü desteklemeyen tarayıcılar içeriyorsa 'Origin' in globalThis özelliğini algılayın ve URL'leri new URL() ile ayrıştırmaya geri dönün:

  • Aynı kaynak yedek (demet kaynaklar): URL dizesini ayıklayın (MessageEvent nesneler için event.origin veya bağlantı öğeleri için candidate.href kullanarak), hem aday hem de güvenilir URL'leri new URL() ile ayrıştırın, "null" opak kaynakları (candidateUrl.origin !== 'null') reddedin ve candidateUrl.origin === trustedUrl.origin karşılaştırın. Opak kaynak, 'https://app.example.com' gibi bir tuple kaynağıyla hiçbir zaman aynı kaynak olmadığından 'null''i hemen reddetmek Origin.isSameOrigin() ile eşleşir.
  • Aynı site yedek (uyarı): URL arayüzü, tarayıcının Genel Ek Listesi'ni göstermez. Origin içermeyen tarayıcılarda, yalnızca kontrol ettiğiniz açık ve bilinen özel kaydedilebilir alan adına karşı son ek eşleme gerçekleştirin (hem protocol hem de tam veya nokta ön ekli hostname eşleşmesini doğrulayarak) ya da güncel bir Herkese Açık Son Ek Listesi kitaplığı kullanın.
  • Opak/Korumalı alana alınmış iframe yedek: Origin içermeyen tarayıcılar, MessageEvent üzerinde farklı opak kaynak kimlikleri göstermez (event.origin her zaman "null" olur). Bilinen bir korumalı alana alınmış <iframe sandbox="allow-scripts"> kaynağından gelen mesajları kasıtlı olarak kabul ediyorsanız event.origin değerini karşılaştırmak yerine event.source === expectedWidgetWindow değerini doğrulayın (veya başlatma sırasında iframe'e aktarılan özel bir MessageChannel MessagePort üzerinden iletişim kurun).
// 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;
  }
}