درحالیکه مقایسه event.origin با یک رشته ثابت و نرمالشده ازنظر درگاه (مانند event.origin === 'https://app.example.com') برای فهرستهای مجاز ساده کار میکند، مقایسههای دستی رشتهها و عبارتهای باقاعده هنگام اعتبارسنجی نشانیهای وب دلخواه، عناصر پیوند DOM، تغییرمسیرهای بین زیردامنهها، یا پیامهای محدودشده در محیط امن ازکار میافتند:
- جعل پیشوند و رشته فرعی: بررسی
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 API (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()هم تنزل طرحhttp:و هم مستأجران همنیا در پسوندهای عمومی مشترک (.co.uk،.github.io) را رد میکند. - از
origin.opaqueوOrigin.from(event)برای زمینههای مات یا جعبه امن استفاده کنید: نشانیهای وبdata:را برای تغییر مسیرها (origin.opaque === true) رد کنید، یاOriginمات استخراجشده ازطریقOrigin.from(event)را هنگام برقراری ارتباط با ابزاره<iframe sandbox="allow-scripts">خاصی کهevent.originسریالیشده آن"null"است سنجاق کنید.
۱. درستیسنجی کردن مبدأ دقیق برای کنشهای ممتاز (Origin.from() و isSameOrigin())
از Origin.from() برای استخراج شیء Origin و مقایسه آن با Origin معتمد بااستفاده از isSameOrigin() استفاده کنید:
- هر دو طرف باید نمونههای
Originباشند:isSameOrigin(other)وisSameSite(other)فقط شیءOriginرا میپذیرند—ارسال رشته خام باعث ایجادTypeErrorمیشود. - ورودیهای
Origin.from()پشتیبانیشده:- رشتههای نشانی وب سریالیشده (برای مثال،
'https://app.example.com:443/path'). توجه داشته باشید که هر رشته نشانی وب نامعتبر (برای مثال'example'، یا رشته'null'سریالیشده توسطevent.originبرای مبدأهای مبهم) باعث میشودOrigin.from()یکTypeErrorایجاد کند. - اشیاء پلاتفرم که مراحل استخراج مبدأ را در مشخصات HTML تعریف میکنند:
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را ارسال کنید.
- رشتههای نشانی وب سریالیشده (برای مثال،
- Handle both browser-dispatched and synthetic
MessageEventinstances: Real browserpostMessageevents carry an internal origin extracted byOrigin.from(event), whereas syntheticnew MessageEvent('message', { origin })events created in unit tests only populate theevent.originstring and throw onOrigin.from(event). بازگشت بهOrigin.from(event.origin)درtry...catchهر دو را بهطور تمیز مدیریت میکند و درعینحال رشتههای نامعتبر یا"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
});
۲. اعتبارسنجی پیوندها و هدایتهای مجدد بین زیردامنهها (isSameSite())
هنگام اعتبارسنجی هدفهای تغییرمسیر (مانند پارامترهای ?returnTo=) یا پیوندهای داخلی در زیردامنههای همان دامنه قابلثبت (برای مثال، https://app.example.co.uk و https://billing.example.co.uk:8443)، از isSameSite() استفاده کنید:
-
isSameSite()مقایسه schemeful same-site را انجام میدهد: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');
۳. Distinguish and Pin Opaque Origins (origin.opaque and 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;
}
}
روالهای مطلوب
- باید هر دو طرف مقایسه را ازطریق
Origin.from()به نمونههایOriginتبدیل کنید و سپسisSameOrigin(other)یاisSameSite(other)را فراخوانی کنید. - باید وقتی لازم است هویت مبدأ مبهم را حفظ کنید،
MessageEventارسالشده از مرورگر را مستقیماً بهOrigin.from(event)ارسال کنید و برای دریافتOriginبافت اجرای فعلی، ازOrigin.from(self)(بهجایLocationیاDocumentکهTypeErrorرا برمیگردانند) استفاده کنید. - باید فراخوانیهای
Origin.from()در ورودیهای غیرقابلاعتماد یا زمان اجرا را در بلوکtry...catchقرار دهید تا نشانیهای وب نامعتبر، رشتههای"null"، یا اشیای پشتیبانینشده بهجای ایجادTypeErrorغیرقابلکنترل، بهطور ایمنfalseرا برگردانند. - دو رشته زمان اجرای
event.originیاurl.originرا مستقیماً بدون رد کردن"null"(origin !== 'null') مقایسه نکنید، زیرا دو مبدأ غیرشفاف نامرتبط هر دو به"null"سریالسازی میشوند. - مبدأها یا سایتها را با
startsWith()،includes()،endsWith()، یا برش نام میزبان.split('.')ساده اعتبارسنجی نکنید.
راهبرد جایگزین
اگر هدف Baseline شما شامل مرورگرهایی است که هنوز از میانای Origin پشتیبانی نمیکنند، 'Origin' in globalThis را تشخیص ویژگی کنید و به تجزیه نشانیهای وب با new URL() برگردید:
- جایگزین هممبدأ (مبدأهای چندتایی): رشته نشانی وب را استخراج کنید (بااستفاده از
event.originبرای اشیایMessageEventیاcandidate.hrefبرای عناصر پیوند)، نشانیهای وب نامزد و مطمئن را باnew URL()تجزیه کنید، مبدأهای مبهم"null"(candidateUrl.origin !== 'null') را رد کنید، وcandidateUrl.origin === trustedUrl.originرا مقایسه کنید. چون مبدأ مات هرگز با مبدأ چندتایی مثل'https://app.example.com'هممبدأ نیست، فوراً'null'منطبق باOrigin.isSameOrigin()را رد میکند. - جایگزین همان سایت (هشدار): میانای
URL«فهرست پسوند عمومی» مرورگر را نمایش نمیدهد. در مرورگرهای بدونOrigin، فقط تطابق پسوند را دربرابر دامنه قابلثبت خصوصی صریح و شناختهشدهای که کنترل میکنید انجام دهید (همprotocolو هم تطابق دقیق یا با پیشوند نقطهhostnameرا درستیسنجی کنید)، یا از کتابخانه «فهرست پسوند عمومی» نگهداریشده استفاده کنید. - جایگزین iframe مات / جعبه شنی: مرورگرهای بدون
Originهویتهای مبدأ مات متمایز را درMessageEventنشان نمیدهند (event.originهمیشه"null"است). اگر پیامها را از<iframe sandbox="allow-scripts">جعبه شنی شناختهشدهای بهطور عمدی میپذیرید، بهجای مقایسهevent.origin،event.source === expectedWidgetWindowرا تأیید کنید (یا ازطریقMessageChannelMessagePortاختصاصی که در مقداردهی اولیه به iframe منتقل شده است ارتباط برقرار کنید).
// 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;
}
}