Meskipun membandingkan event.origin dengan literal string tetap yang dinormalisasi port (seperti event.origin === 'https://app.example.com') berfungsi untuk daftar yang diizinkan sederhana, perbandingan string manual dan ekspresi reguler akan gagal saat memvalidasi URL arbitrer, elemen link DOM, pengalihan lintas subdomain, atau pesan dalam sandbox:
- Spoofing awalan dan substring: Pemeriksaan
url.startsWith('https://app.example.com')diabaikan oleh'https://app.example.com.attacker.example'. - URL yang tidak dinormalisasi: URL lengkap dengan jalur, string kueri, atau port default eksplisit (
'https://app.example.com:443/path') tidak cocok dengan'https://app.example.com'dalam kesetaraan string tanpa penguraian dan normalisasi terlebih dahulu. - Kesalahan Daftar Akhiran Publik: Memisahkan nama host di
.untuk memeriksa hubungan situs yang sama akan gagal pada akhiran publik multi-label (seperti.co.ukatau domain hosting bersama seperti.github.io) dan mengabaikan ketidakcocokan skema (http:vs.https:). - Tabrakan origin
"null"buram: Iframe dengan sandbox (<iframe sandbox="allow-scripts">) dan URLdata:melakukan serialisasievent.originke string literal"null". Membandingkan dua origin runtime yang diserialisasi denganoriginA === originBakan menyamakan dua konteks buram yang tidak terkait ("null" === "null") secara keliru.
Origin API (Origin.from(), isSameOrigin(), isSameSite(), dan opaque) menyediakan perbandingan origin dan situs yang sama dengan skema yang terstruktur dan sesuai spesifikasi, yang didukung oleh parser URL browser dan Daftar Sufiks Publik.
Memilih perbandingan yang tepat untuk skenario Anda
- Gunakan
isSameOrigin()untuk batas hak istimewa yang ketat: Wajibkan pencocokan(scheme, host, port)yang sama persis saat menangani perintahpostMessagelintas jendela (seperti checkout tersemat atau pop-up autentikasi) atau memutuskan apakah akan melampirkan kredensial. Bahkan subdomain yang berbeda (https://blog.example.comvs.https://app.example.com) adalah origin yang berbeda dan harus ditolak agar kerentanan pada satu subdomain tidak dapat mengontrol subdomain lainnya. - Gunakan
isSameSite()untuk navigasi lintas subdomain dan klasifikasi link: Izinkan subdomain atau port apa pun yang menggunakan skema dan domain yang dapat didaftarkan yang sama saat memvalidasi URL pengalihan pasca-login (?returnTo=https://billing.example.co.uk/orders) atau mengklasifikasikan link sebagai internal vs. eksternal.isSameSite()menolak downgrade skemahttp:dan tenant seinduk pada akhiran publik bersama (.co.uk,.github.io). - Gunakan
origin.opaquedanOrigin.from(event)untuk konteks buram atau dengan sandbox: Tolak URLdata:untuk pengalihan (origin.opaque === true), atau sematkanOriginburam yang diekstrak melaluiOrigin.from(event)saat berkomunikasi dengan widget<iframe sandbox="allow-scripts">tertentu yangevent.origin-nya diserialisasi adalah"null".
1. Memverifikasi Origin yang Tepat untuk Tindakan Istimewa (Origin.from() dan isSameOrigin())
Gunakan Origin.from() untuk mengekstrak objek Origin dan membandingkannya dengan Origin tepercaya menggunakan isSameOrigin():
- Kedua sisi harus berupa instance
Origin:isSameOrigin(other)danisSameSite(other)hanya menerima objekOrigin—meneruskan string mentah akan menampilkanTypeError. - Input
Origin.from()yang didukung:- String URL yang diserialisasi (misalnya,
'https://app.example.com:443/path'). Perhatikan bahwa string URL yang tidak valid—misalnya'example', atau string'null'yang diserialisasi olehevent.originuntuk origin opaque—menyebabkanOrigin.from()menampilkanTypeError. - Objek platform yang menentukan langkah-langkah ekstraksi origin dalam spesifikasi HTML:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>),Windoworigin yang sama atauWorkerGlobalScope(Origin.from(self)), dan instanceMessageEventyang dikirim browser (Origin.from(event)). - Objek seperti
Location,WorkerLocation,Document,Request, danResponsetidak menentukan langkah-langkah ekstraksi origin dan menampilkanTypeErrorjika diteruskan langsung keOrigin.from()—teruskanself(untuk konteks saat ini) ataurequest.url/response.url.
- String URL yang diserialisasi (misalnya,
- Menangani instance
MessageEventyang dikirim browser dan sintetis: PeristiwapostMessagebrowser sungguhan membawa origin internal yang diekstrak olehOrigin.from(event), sedangkan peristiwanew MessageEvent('message', { origin })sintetis yang dibuat dalam pengujian unit hanya mengisi stringevent.origindan menampilkan error diOrigin.from(event). Penggunaan penggantian keOrigin.from(event.origin)di dalamtry...catchakan menangani keduanya dengan baik, sekaligus tetap menolak string yang tidak valid atau"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. Memvalidasi Pengalihan dan Link Lintas-Subdomain (isSameSite())
Saat memvalidasi target pengalihan (seperti parameter ?returnTo=) atau link internal di seluruh subdomain dari domain yang dapat didaftarkan yang sama (misalnya, https://app.example.co.uk dan https://billing.example.co.uk:8443), gunakan isSameSite():
isSameSite()melakukan perbandingan schemeful same-site:https://sub.example.comdanhttp://sub.example.combukan situs yang sama karena skemanya berbeda.isSameSite()menggunakan Daftar Akhiran Publik bawaan browser, sehingga tenant yang berbeda pada akhiran publik bersama (sepertihttps://tenant-a.github.iodanhttps://tenant-b.github.io, atauhttps://a.co.ukdanhttps://b.co.uk) dievaluasi kefalse.
// 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. Membedakan dan Menyematkan Origin Opaque (origin.opaque dan Origin.from(event))
Iframe dengan sandbox (<iframe sandbox="allow-scripts">), URL data:, dan new Origin() menghasilkan origin opaque (origin.opaque === true). Meskipun serialisasi string-nya selalu "null", Origin.from(event) mengekstrak origin opaque dasar pengirim yang sebenarnya dari MessageEvent yang dikirim browser:
- Dua pesan yang dikirim dari iframe dengan sandbox yang sama menghasilkan objek
OriginyangisSameOrigin()satu sama lain (true). - Pesan yang dikirim dari iframe dengan sandbox yang berbeda—atau panggilan
Origin.from('data:...')/new Origin()terpisah—menghasilkan origin buram berbeda yang mengevaluasiisSameOrigin()kefalse, sehingga mencegah spoofing"null" === "null"di seluruh konteks buram yang tidak terkait. new Origin()membuat origin buram baru yang unik dan tidak cocok dengan apa pun kecuali dirinya sendiri, yang berguna sebagai sentinel tolak semua default sebelum origin peer yang diharapkan disematkan sekali saat inisialisasi.
// 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;
}
}
Praktik terbaik
- LAKUKAN konversi kedua sisi perbandingan ke instance
OriginmelaluiOrigin.from()sebelum memanggilisSameOrigin(other)atauisSameSite(other). - LAKUKAN penerusan
MessageEventyang dikirim browser langsung keOrigin.from(event)saat Anda perlu mempertahankan identitas origin buram, dan gunakanOrigin.from(self)(bukanLocationatauDocument, yang menampilkanTypeError) untuk mendapatkanOriginkonteks eksekusi saat ini. - LAKUKAN penggabungan panggilan
Origin.from()pada input yang tidak tepercaya atau runtime dalam bloktry...catchsehingga URL yang tidak valid, string"null", atau objek yang tidak didukung akan menampilkanfalsedengan aman, bukan menampilkanTypeErroryang tidak tertangkap. - JANGAN membandingkan dua string
event.originatauurl.originruntime secara langsung tanpa terlebih dahulu menolak"null"(origin !== 'null'), karena dua origin buram yang tidak terkait akan diserialisasi ke"null". - JANGAN memvalidasi origin atau situs dengan
startsWith(),includes(),endsWith(), atau pemotongan nama host.split('.')yang naif.
Strategi penggantian
Jika target Baseline Anda mencakup browser yang belum mendukung antarmuka Origin, deteksi fitur 'Origin' in globalThis dan gunakan pengganti untuk mengurai URL dengan new URL():
- Penggantian origin yang sama (origin tuple): Ekstrak string URL (menggunakan
event.originuntuk objekMessageEventataucandidate.hrefuntuk elemen link), uraikan URL kandidat dan tepercaya dengannew URL(), tolak origin buram"null"(candidateUrl.origin !== 'null'), dan bandingkancandidateUrl.origin === trustedUrl.origin. Karena origin buram tidak pernah memiliki origin yang sama dengan origin tuple seperti'https://app.example.com', penolakan langsung terhadap'null'akan cocok denganOrigin.isSameOrigin(). - Penggantian situs yang sama (peringatan): Antarmuka
URLtidak mengekspos Daftar Suffix Publik browser. Di browser tanpaOrigin, hanya lakukan pencocokan akhiran terhadap domain pribadi eksplisit dan dikenal yang dapat didaftarkan yang Anda kontrol (memverifikasiprotocoldan kecocokanhostnameyang sama persis atau diawali titik), atau gunakan library Daftar Akhiran Publik yang dikelola. - Penggantian iframe buram/di-sandbox: Browser tanpa
Origintidak mengekspos identitas origin buram yang berbeda diMessageEvent(event.originselalu"null"). Jika Anda sengaja menerima pesan dari<iframe sandbox="allow-scripts">yang di-sandbox dan diketahui, verifikasievent.source === expectedWidgetWindow(atau berkomunikasi melaluiMessageChannelMessagePortkhusus yang ditransfer ke iframe saat inisialisasi), bukan membandingkanevent.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;
}
}