השוואה בין event.origin לבין מחרוזת מילולית קבועה עם פורט מנורמל (כמו event.origin === 'https://app.example.com') מתאימה לרשימות היתרים פשוטות, אבל השוואות ידניות של מחרוזות וביטויים רגולריים לא פועלות כשמאמתים כתובות URL שרירותיות, רכיבי קישור של DOM, הפניות אוטומטיות בין תת-דומיינים או הודעות בארגז חול:
- זיוף של קידומת ומחרוזת משנה: בדיקה של
url.startsWith('https://app.example.com')נעקפת על ידי'https://app.example.com.attacker.example'. - כתובות URL לא מנורמלות: כתובות URL מלאות עם נתיבים, מחרוזות שאילתה או יציאות ברירת מחדל מפורשות (
'https://app.example.com:443/path') לא תואמות ל-'https://app.example.com'לפי שוויון מחרוזות בלי ניתוח ונרמול קודם. - טעויות ברשימת הסיומות הציבוריות: פיצול שמות מארחים ב-
.כדי לבדוק קשרים של אותו אתר נכשל בסיומות ציבוריות עם כמה תוויות (כמו.co.ukאו דומיינים מתארחים כמו.github.io) ומתעלם מחוסר התאמה בסכימות (http:לעומתhttps:). - התנגשויות של מקורות
"null"אטומים: מסגרות iframe בארגז חול (<iframe sandbox="allow-scripts">) וכתובות URL מסוגdata:עוברות סריאליזציהevent.originלמחרוזת המילולית"null". השוואה בין שני מקורות של זמן ריצה שעברו סריאליזציה באמצעותoriginA === originBמשווה באופן שגוי בין שני הקשרים אטומים לא קשורים ("null" === "null").
Origin API (Origin.from(), isSameOrigin(), isSameSite() ו-opaque) מספק השוואות מובנות, תואמות למפרט, של מקורות ושל אתרים זהים עם סכימה, שמגובות על ידי מנתח כתובות ה-URL של הדפדפן ורשימת הסיומות הציבוריות.
בחירת ההשוואה המתאימה לתרחיש שלכם
- שימוש ב-
isSameOrigin()לגבולות הרשאות מחמירים: דורשים התאמה מדויקת של(scheme, host, port)כשמטפלים בפקודותpostMessageבין חלונות (כמו חלון קופץ מוטמע של תשלום או אימות) או כשמחליטים אם לצרף פרטי כניסה. גם תת-דומיין שונה (https://blog.example.comלעומתhttps://app.example.com) נחשב למקור שונה, וצריך לדחות אותו כדי שנקודת חולשה בתת-דומיין אחד לא תוכל לשלוט בתת-דומיין אחר. - שימוש ב-
isSameSite()לניווט בין תת-דומיינים ולסיווג קישורים: אפשר להשתמש בכל תת-דומיין או יציאה שחולקים את אותה סכימה ודומיין שניתן לרשום כשמאמתים כתובות URL להפניה אוטומטית אחרי ההתחברות (?returnTo=https://billing.example.co.uk/orders) או כשמסווגים קישורים כפנימיים לעומת חיצוניים. isSameSite()דוחה גם שדרוגים לאחור של סכימתhttp:וגם דיירים אחים בסיומות ציבוריות משותפות (.co.uk,.github.io). - משתמשים ב-
origin.opaqueוב-Origin.from(event)בהקשרים אטומים או בארגז חול: דוחים כתובות URL מסוגdata:להפניות אוטומטיות (origin.opaque === true), או מצמידים את ה-Originהאטום שחולץ באמצעותOrigin.from(event)כשמתקשרים עם ווידג'ט<iframe sandbox="allow-scripts">ספציפי שה-event.originשלו שעבר סריאליזציה הוא"null".
1. אימות מקורות מדויקים לפעולות עם הרשאות (Origin.from() ו-isSameOrigin())
משתמשים ב-Origin.from() כדי לחלץ אובייקט Origin ולהשוות אותו ל-Origin מהימן באמצעות isSameOrigin():
- שני הצדדים חייבים להיות מופעים של
Origin:isSameOrigin(other)ו-isSameSite(other)מקבלים רק אובייקטOrigin– העברת מחרוזת גולמית גורמת ל-TypeError. - קלט נתמך ב-
Origin.from():- מחרוזות של כתובות URL שעברו סריאליזציה (לדוגמה,
'https://app.example.com:443/path'). שימו לב שכל מחרוזת של כתובת URL לא תקינה – למשל'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לא מגדירים שלבים לחילוץ מקורות, ומופיעה השגיאהTypeErrorאם מעבירים אותם ישירות אלOrigin.from(). במקום זאת, צריך להעביר אתself(להקשר הנוכחי) או אתrequest.url/response.url.
- מחרוזות של כתובות URL שעברו סריאליזציה (לדוגמה,
- טיפול גם במופעי
MessageEventשנשלחו מהדפדפן וגם במופעים סינתטיים: אירועיpostMessageאמיתיים בדפדפן כוללים מקור פנימי שחולץ על ידיOrigin.from(event), ואילו אירועיnew MessageEvent('message', { origin })סינתטיים שנוצרו בבדיקות יחידה מאכלסים רק את המחרוזתevent.originומפעילים אתOrigin.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
});
2. אימות של הפניות אוטומטיות וקישורים בין תת-דומיינים (isSameSite())
כשמאמתים יעדי הפניה אוטומטית (כמו פרמטרים של ?returnTo=) או קישורים פנימיים בין תת-דומיינים של אותו דומיין שניתן לרשום (לדוגמה, https://app.example.co.uk ו-https://billing.example.co.uk:8443), צריך להשתמש ב-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">), כתובות URL מסוג data: ו-new Origin() יוצרים מקורות אטומים (origin.opaque === true). למרות שהסריאליזציה של המחרוזת שלהם היא תמיד "null", Origin.from(event) מחלץ את המקור האטום הבסיסי בפועל של השולח מ-MessageEvent שנשלח על ידי הדפדפן:
- שתי הודעות שנשלחות מאותו iframe (same) בארגז חול מניבות אובייקטים מסוג
OriginשהםisSameOrigin()זה עם זה (true). - הודעות שנשלחות מ-iframes שונים בארגז חול (sandbox) – או קריאות נפרדות ל-
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באמצעותOrigin.from()לפני שקוראים ל-isSameOrigin(other)או ל-isSameSite(other). - מומלץ להעביר
MessageEventשנשלח על ידי הדפדפן ישירות אלOrigin.from(event)כשצריך לשמור על זהות המקור האטומה, ולהשתמש ב-Origin.from(self)(במקום ב-Locationאו ב-Document, שגורמים ל-TypeError) כדי לקבל את ה-Originשל הקשר הביצוע הנוכחי. - מומלץ לעטוף קריאות ל-
Origin.from()בקלט לא מהימן או בקלט בזמן ריצה בבלוקtry...catch, כדי שכתובות URL לא חוקיות, מחרוזות"null"או אובייקטים לא נתמכים יחזירוfalseבאופן בטוח במקום להפעילTypeErrorשלא נתפס. - אל תשוו בין שתי מחרוזות
event.originאוurl.originשל זמן ריצה באופן ישיר בלי לדחות קודם את"null"(origin !== 'null'), כי שני מקורות אטומים לא קשורים עוברים סריאליזציה ל-"null". - אל תאמתו מקורות או אתרים באמצעות
startsWith(),includes(),endsWith()או חיתוך פשוט של שם המארח.split('.').
אסטרטגיית גיבוי
אם יעד ה-Baseline כולל דפדפנים שעדיין לא תומכים בממשק Origin, צריך לזהות את התכונה 'Origin' in globalThis ולחזור לניתוח כתובות URL באמצעות new URL():
- חלופה לאותו מקור (מקורות של טופלים): מחלצים את מחרוזת כתובת ה-URL (באמצעות
event.originלאובייקטים שלMessageEventאוcandidate.hrefלאלמנטים של קישורים), מנתחים את כתובת ה-URL המועמדת וכתובת ה-URL המהימנה באמצעות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.source === expectedWidgetWindow(או לתקשר דרךMessageChannelMessagePortייעודי שמועבר ל-iframe באתחול) במקום להשוות אתevent.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;
}
}