वेब ऑरिजिन और साइटों की तुलना करना और उनकी पुष्टि करना

वेब की सुविधाएं: ऑरिजिन

event.origin की तुलना, पोर्ट-नॉर्मलाइज़ की गई स्ट्रिंग लिटरल (जैसे, event.origin === 'https://app.example.com') से करने पर, अनुमति वाली सामान्य सूचियों के लिए काम करती है. हालांकि, मैन्युअल तरीके से स्ट्रिंग की तुलना करने और रेगुलर एक्सप्रेशन का इस्तेमाल करने पर, आर्बिट्रेरी यूआरएल, डीओएम लिंक एलिमेंट, क्रॉस-सबडोमेन रीडायरेक्ट या सैंडबॉक्स किए गए मैसेज की पुष्टि नहीं हो पाती:

  • प्रीफ़िक्स और सबस्ट्रिंग स्पूफ़िंग: url.startsWith('https://app.example.com') की जांच को 'https://app.example.com.attacker.example' से बायपास किया जाता है.
  • अननॉर्मलाइज़्ड यूआरएल: पाथ, क्वेरी स्ट्रिंग या साफ़ तौर पर बताए गए डिफ़ॉल्ट पोर्ट ('https://app.example.com:443/path') वाले फ़ुल यूआरएल, स्ट्रिंग की समानता के तहत 'https://app.example.com' से मैच नहीं होते. इसके लिए, पहले उन्हें पार्स और नॉर्मलाइज़ करना ज़रूरी है.
  • पब्लिक सफ़िक्स लिस्ट से जुड़ी गलतियां: सेम-साइट रिलेशनशिप की जांच करने के लिए, . पर होस्टनेम को अलग-अलग करने की प्रोसेस, कई लेबल वाले पब्लिक सफ़िक्स (जैसे, .co.uk या .github.io जैसे शेयर किए गए होस्टिंग डोमेन) पर काम नहीं करती. साथ ही, यह स्कीम के मेल न खाने (http: बनाम https:) को अनदेखा करती है.
  • ओपेक "null" ऑरिजिन कोलिज़न: सैंडबॉक्स किए गए iframe (<iframe sandbox="allow-scripts">) और data: यूआरएल, event.origin को लिटरल स्ट्रिंग "null" में सीरियलाइज़ करते हैं. originA === originB का इस्तेमाल करके, सीरियलाइज़ किए गए दो रनटाइम ऑरिजिन की तुलना करने पर, दो अलग-अलग ओपेक कॉन्टेक्स्ट ("null" === "null") को गलत तरीके से बराबर माना जाता है.

Origin एपीआई (Origin.from(), isSameOrigin(), isSameSite(), और opaque) स्ट्रक्चर्ड और स्पेसिफ़िकेशन के मुताबिक ऑरिजिन और स्कीम के हिसाब से सेम-साइट की तुलना करने की सुविधा देता है. इसके लिए, ब्राउज़र के यूआरएल पार्सर और पब्लिक सफ़िक्स लिस्ट का इस्तेमाल किया जाता है.

अपने सिनेरियो के लिए सही तुलना चुनना

  • अनुमतियों की सख्त सीमाओं के लिए, isSameOrigin() का इस्तेमाल करें: क्रॉस-विंडो postMessage कमांड (जैसे, एम्बेड किया गया चेकआउट या पुष्टि करने वाला पॉप-अप) को मैनेज करते समय या क्रेडेंशियल अटैच करने का फ़ैसला लेते समय, एग्ज़ैक्ट (scheme, host, port) मैच की ज़रूरत होती है. यहां तक कि अलग सबडोमेन (https://blog.example.com बनाम https://app.example.com) भी एक अलग ऑरिजिन है और इसे अस्वीकार किया जाना चाहिए, ताकि एक सबडोमेन पर मौजूद जोखिम दूसरे को कंट्रोल न कर सके.
  • एक से दूसरे सबडोमेन पर जाने और लिंक को अलग-अलग कैटगरी में बांटने के लिए, isSameSite() का इस्तेमाल करें: लॉगिन करने के बाद रीडायरेक्ट करने वाले यूआरएल (?returnTo=https://billing.example.co.uk/orders) की पुष्टि करते समय या लिंक को इंटरनल बनाम एक्सटर्नल के तौर पर अलग-अलग कैटगरी में बांटते समय, एक ही स्कीम और रजिस्टर किए जा सकने वाले डोमेन को शेयर करने वाले किसी भी सबडोमेन या पोर्ट को अनुमति दें. isSameSite(), शेयर किए गए सार्वजनिक सफ़िक्स (.co.uk, .github.io) पर, http: स्कीम के डाउनग्रेड और सिबलिंग टेनेंट, दोनों को अस्वीकार करता है.
  • ओपेक या सैंडबॉक्स किए गए कॉन्टेक्स्ट के लिए, origin.opaque और Origin.from(event) का इस्तेमाल करें: रीडायरेक्ट (origin.opaque === true) के लिए, data: यूआरएल अस्वीकार करें या किसी खास <iframe sandbox="allow-scripts"> विजेट के साथ कम्यूनिकेट करते समय, Origin.from(event) के ज़रिए निकाले गए ओपेक Origin को पिन करें. इस विजेट का सीरियलाइज़ किया गया event.origin, "null" है.

1. खास कार्रवाइयों (Origin.from() और isSameOrigin()) के लिए, सटीक ऑरिजिन की पुष्टि करना

Origin.from() का इस्तेमाल करके, Origin ऑब्जेक्ट को निकालें और isSameOrigin() का इस्तेमाल करके, भरोसेमंद Origin से उसकी तुलना करें:

  • दोनों साइड Origin के इंस्टेंस होने चाहिए: isSameOrigin(other) और isSameSite(other) सिर्फ़ Origin ऑब्जेक्ट को स्वीकार करते हैं—रॉ स्ट्रिंग पास करने पर TypeError दिखता है.
  • इस्तेमाल किए जा सकने वाले Origin.from() इनपुट:
    • सीरियलाइज़ की गई यूआरएल स्ट्रिंग (उदाहरण के लिए, 'https://app.example.com:443/path'). ध्यान दें कि कोई भी अमान्य यूआरएल स्ट्रिंग—उदाहरण के लिए, 'example' या ओपेक ऑरिजिन के लिए event.origin से सीरियलाइज़ की गई 'null' स्ट्रिंग—Origin.from() को TypeError थ्रो करने का कारण बनती है.
    • एचटीएमएल स्पेसिफ़िकेशन में ऑरिजिन-एक्सट्रैक्शन के चरणों को तय करने वाले प्लैटफ़ॉर्म ऑब्जेक्ट: Origin, URL, HTMLAnchorElement (<a>), HTMLAreaElement (<area>), सेम-ऑरिजिन Window या WorkerGlobalScope (Origin.from(self)), और ब्राउज़र से भेजे गए MessageEvent इंस्टेंस (Origin.from(event)).
    • Location, WorkerLocation, Document, Request, और Response जैसे ऑब्जेक्ट, ऑरिजिन निकालने के चरणों को तय नहीं करते. साथ ही, सीधे Origin.from() को पास किए जाने पर, TypeError थ्रो करते हैं. इसके बजाय, self (मौजूदा कॉन्टेक्स्ट के लिए) या request.url / response.url पास करें.
  • ब्राउज़र से भेजे गए और सिंथेटिक, दोनों तरह के MessageEvent इंस्टेंस मैनेज करें: असली ब्राउज़र postMessage इवेंट में, Origin.from(event) से निकाला गया इंटरनल ऑरिजिन होता है. वहीं, यूनिट टेस्ट में बनाए गए सिंथेटिक new MessageEvent('message', { origin }) इवेंट, सिर्फ़ event.origin स्ट्रिंग को पॉप्युलेट करते हैं और Origin.from(event) पर थ्रो करते हैं. try...catch के अंदर Origin.from(event.origin) का इस्तेमाल करने से, दोनों को आसानी से मैनेज किया जा सकता है. साथ ही, अमान्य या "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
});

रजिस्टर किए जा सकने वाले एक ही डोमेन (उदाहरण के लिए, https://app.example.co.uk और https://billing.example.co.uk:8443) के सबडोमेन में, रीडायरेक्ट टारगेट (जैसे कि ?returnTo= पैरामीटर) या इंटरनल लिंक की पुष्टि करते समय, isSameSite() का इस्तेमाल करें:

  • isSameSite(), स्कीमफ़ुल सेम-साइट की तुलना करता है: https://sub.example.com और http://sub.example.com सेम-साइट नहीं हैं, क्योंकि उनकी स्कीम अलग-अलग हैं.
  • isSameSite(), ब्राउज़र में पहले से मौजूद सार्वजनिक सफ़िक्स सूची का इस्तेमाल करता है. इसलिए, शेयर किए गए सार्वजनिक सफ़िक्स (जैसे, https://tenant-a.github.io और https://tenant-b.github.io या https://a.co.uk और https://b.co.uk) पर अलग-अलग टेनेंट, 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. ओपेक ऑरिजिन (origin.opaque और Origin.from(event)) को अलग-अलग करें और पिन करें

सैंडबॉक्स किए गए iframe (<iframe sandbox="allow-scripts">), data: यूआरएल, और new Origin(), ओपेक ऑरिजिन (origin.opaque === true) जनरेट करते हैं. हालांकि, उनकी स्ट्रिंग सीरियलाइज़ेशन हमेशा "null" होती है, लेकिन Origin.from(event), ब्राउज़र से भेजे गए MessageEvent से, भेजने वाले के असल ओपेक ऑरिजिन को निकालता है:

  • एक ही सैंडबॉक्स किए गए iframe से भेजे गए दो मैसेज, Origin ऑब्जेक्ट जनरेट करते हैं. ये ऑब्जेक्ट एक-दूसरे के साथ isSameOrigin() होते हैं (true).
  • अलग-अलग सैंडबॉक्स किए गए iframe या अलग-अलग Origin.from('data:...') / new Origin() कॉल से भेजे गए मैसेज, अलग-अलग ओपेक ऑरिजिन जनरेट करते हैं. ये ऑरिजिन, isSameOrigin() को false के तौर पर आकलन करते हैं. इससे, बिना किसी संबंध वाले ओपेक कॉन्टेक्स्ट में "null" === "null" स्पूफ़िंग को रोका जा सकता है.
  • new Origin() एक नया और यूनीक ओपेक ऑरिजिन बनाता है, जो सिर्फ़ खुद से मैच करता है. यह डिफ़ॉल्ट रूप से, सभी को अस्वीकार करने वाले सेंटिनल के तौर पर काम करता है. यह तब तक काम करता है, जब तक कि शुरू होने पर, उम्मीद के मुताबिक पीयर ऑरिजिन को एक बार पिन नहीं किया जाता.
// 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;
  }
}

सबसे सही तरीके

  • isSameOrigin(other) या isSameSite(other) को कॉल करने से पहले, तुलना के दोनों पक्षों को Origin.from() के ज़रिए Origin इंस्टेंस में बदलें.
  • जब आपको ओपेक ऑरिजिन आइडेंटिटी को सुरक्षित रखना हो, तब ब्राउज़र से भेजे गए MessageEvent को सीधे Origin.from(event) पर पास करें. साथ ही, मौजूदा एक्ज़ीक्यूशन कॉन्टेक्स्ट का Origin पाने के लिए, Origin.from(self) का इस्तेमाल करें. Location या Document का इस्तेमाल न करें, क्योंकि इनसे TypeError थ्रो होता है.
  • ज़रूर करें: गैर-भरोसेमंद या रनटाइम इनपुट पर Origin.from() कॉल को try...catch ब्लॉक में रैप करें, ताकि अमान्य यूआरएल, "null" स्ट्रिंग या काम न करने वाले ऑब्जेक्ट, अनकैच किए गए TypeError को थ्रो करने के बजाय, सुरक्षित तरीके से false को रिटर्न करें.
  • "null" (origin !== 'null') को अस्वीकार किए बिना, दो रनटाइम event.origin या url.origin स्ट्रिंग की तुलना सीधे तौर पर न करें. ऐसा इसलिए, क्योंकि दो अलग-अलग ओपेक ऑरिजिन, दोनों "null" में सीरियलाइज़ होते हैं.
  • startsWith(), includes(), endsWith() या होस्टनेम को स्लाइस करने वाले सामान्य .split('.') का इस्तेमाल करके, ऑरिजिन या साइटों की पुष्टि न करें.

फ़ॉलबैक रणनीति

अगर आपके बेसलाइन टारगेट में ऐसे ब्राउज़र शामिल हैं जो अब तक Origin इंटरफ़ेस के साथ काम नहीं करते हैं, तो 'Origin' in globalThis की सुविधा का पता लगाएं. इसके बाद, new URL() की मदद से यूआरएल को पार्स करें:

  • एक ही ऑरिजिन वाला फ़ॉलबैक (टपल ऑरिजिन): यूआरएल स्ट्रिंग को एक्सट्रैक्ट करें. इसके लिए, MessageEvent ऑब्जेक्ट के लिए event.origin या लिंक एलिमेंट के लिए candidate.href का इस्तेमाल करें. इसके बाद, new URL() का इस्तेमाल करके, कैंडिडेट और भरोसेमंद यूआरएल, दोनों को पार्स करें. साथ ही, "null" ओपेक ऑरिजिन (candidateUrl.origin !== 'null') को अस्वीकार करें और candidateUrl.origin === trustedUrl.origin की तुलना करें. ओपेक ऑरिजिन, 'https://app.example.com' जैसे टपल ऑरिजिन के साथ कभी भी सेम-ऑरिजिन नहीं होता. इसलिए, 'null' को तुरंत अस्वीकार करने से Origin.isSameOrigin() मैच होता है.
  • Same-site फ़ॉलबैक (चेतावनी): URL इंटरफ़ेस, ब्राउज़र की सार्वजनिक सफ़िक्स सूची को ज़ाहिर नहीं करता. Origin के बिना ब्राउज़र में, सिर्फ़ ऐसे साफ़ तौर पर जाने-पहचाने निजी रजिस्टर किए जा सकने वाले डोमेन के लिए सफ़िक्स मैचिंग करें जिसे आप कंट्रोल करते हैं. इसके लिए, protocol और सटीक या डॉट-प्रीफ़िक्स वाले hostname मैच, दोनों की पुष्टि करें. इसके अलावा, मैनेज की जा रही सार्वजनिक सफ़िक्स सूची की लाइब्रेरी का इस्तेमाल करें.
  • ओपेक / सैंडबॉक्स किए गए iframe फ़ॉलबैक: जिन ब्राउज़र में Origin नहीं होता वे MessageEvent पर अलग-अलग ओपेक ऑरिजिन आइडेंटिटी को ज़ाहिर नहीं करते (event.origin हमेशा "null" होता है). अगर आपको किसी जाने-पहचाने सैंडबॉक्स किए गए <iframe sandbox="allow-scripts"> से मैसेज स्वीकार करने हैं, तो event.origin की तुलना करने के बजाय, event.source === expectedWidgetWindow की पुष्टि करें या शुरू करने पर iframe में ट्रांसफ़र किए गए खास MessageChannel MessagePort पर कम्यूनिकेट करें.
// 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;
  }
}