Mặc dù việc so sánh event.origin với một giá trị cố định dạng chuỗi cố định, được chuẩn hoá theo cổng (chẳng hạn như event.origin === 'https://app.example.com') hoạt động cho các danh sách cho phép đơn giản, nhưng các phép so sánh chuỗi thủ công và biểu thức chính quy sẽ không hoạt động khi xác thực URL tuỳ ý, phần tử liên kết DOM, lệnh chuyển hướng giữa các miền con hoặc thông báo trong hộp cát:
- Giả mạo tiền tố và chuỗi con: Việc kiểm tra
url.startsWith('https://app.example.com')bị'https://app.example.com.attacker.example'bỏ qua. - URL chưa chuẩn hoá: URL đầy đủ có đường dẫn, chuỗi truy vấn hoặc cổng mặc định rõ ràng (
'https://app.example.com:443/path') không khớp với'https://app.example.com'theo tính bằng nhau của chuỗi nếu không phân tích cú pháp và chuẩn hoá trước. - Lỗi Danh sách hậu tố công khai: Việc phân tách tên máy chủ trên
.để kiểm tra mối quan hệ cùng trang web sẽ không thành công đối với các hậu tố công khai có nhiều nhãn (chẳng hạn như.co.ukhoặc tên miền được lưu trữ dùng chung như.github.io) và bỏ qua các trường hợp không khớp về giao thức (http:so vớihttps:). - Xung đột nguồn gốc
"null"mờ: Các iframe hộp cát (<iframe sandbox="allow-scripts">) và URLdata:chuyển đổi tuần tựevent.originthành chuỗi ký tự"null". Việc so sánh hai nguồn gốc thời gian chạy được chuyển đổi tuần tự bằngoriginA === originBsẽ đánh đồng hai ngữ cảnh mờ không liên quan ("null" === "null").
Origin API (Origin.from(), isSameOrigin(), isSameSite() và opaque) cung cấp các phép so sánh có cấu trúc, tuân thủ quy cách về nguồn gốc và cùng trang web theo lược đồ, được hỗ trợ bởi trình phân tích cú pháp URL của trình duyệt và Danh sách hậu tố công khai.
Chọn phép so sánh phù hợp với tình huống của bạn
- Sử dụng
isSameOrigin()cho ranh giới đặc quyền nghiêm ngặt: Yêu cầu khớp chính xác(scheme, host, port)khi xử lý các lệnhpostMessagetrên nhiều cửa sổ (chẳng hạn như cửa sổ bật lên thanh toán hoặc xác thực được nhúng) hoặc quyết định xem có đính kèm thông tin xác thực hay không. Ngay cả một miền con khác (https://blog.example.comso vớihttps://app.example.com) cũng là một nguồn gốc khác và phải bị từ chối để lỗ hổng bảo mật trên một miền con không thể kiểm soát miền con khác. - Sử dụng
isSameSite()để điều hướng giữa các miền con và phân loại đường liên kết: Cho phép mọi miền con hoặc cổng có cùng giao thức và miền có thể đăng ký khi xác thực URL chuyển hướng sau khi đăng nhập (?returnTo=https://billing.example.co.uk/orders) hoặc phân loại đường liên kết là nội bộ hay bên ngoài.isSameSite()từ chối cả việc hạ cấp lược đồhttp:và đối tượng thuê đồng cấp trên các hậu tố công khai dùng chung (.co.uk,.github.io). - Sử dụng
origin.opaquevàOrigin.from(event)cho bối cảnh mờ hoặc hộp cát: Từ chối URLdata:để chuyển hướng (origin.opaque === true) hoặc ghimOriginmờ được trích xuất quaOrigin.from(event)khi giao tiếp với một tiện ích<iframe sandbox="allow-scripts">cụ thể cóevent.originđược chuyển đổi tuần tự là"null".
1. Xác minh nguồn gốc chính xác cho các hành động đặc quyền (Origin.from() và isSameOrigin())
Sử dụng Origin.from() để trích xuất đối tượng Origin và so sánh đối tượng đó với Origin đáng tin cậy bằng isSameOrigin():
- Cả hai bên phải là thực thể
Origin:isSameOrigin(other)vàisSameSite(other)chỉ chấp nhận đối tượngOrigin– việc truyền chuỗi thô sẽ gửiTypeError. - Dữ liệu đầu vào
Origin.from()được hỗ trợ:- Chuỗi URL được chuyển đổi tuần tự (ví dụ:
'https://app.example.com:443/path'). Xin lưu ý rằng mọi chuỗi URL không hợp lệ (ví dụ:'example'hoặc chuỗi'null'đượcevent.originchuyển đổi tuần tự cho các nguồn gốc không rõ ràng) đều khiếnOrigin.from()gửi mộtTypeError. - Các đối tượng nền tảng xác định các bước trích xuất nguồn gốc trong thông số kỹ thuật HTML:
Origin,URL,HTMLAnchorElement(<a>),HTMLAreaElement(<area>),WindowhoặcWorkerGlobalScope(Origin.from(self)) cùng nguồn gốc và các thực thểMessageEventdo trình duyệt gửi (Origin.from(event)). - Các đối tượng như
Location,WorkerLocation,Document,RequestvàResponsekhông xác định các bước trích xuất nguồn gốc và gửiTypeErrornếu được truyền trực tiếp đếnOrigin.from()— thay vào đó, hãy truyềnself(cho ngữ cảnh hiện tại) hoặcrequest.url/response.url.
- Chuỗi URL được chuyển đổi tuần tự (ví dụ:
- Xử lý cả thực thể do trình duyệt gửi và thực thể tổng hợp
MessageEvent: Các sự kiệnpostMessagethực tế của trình duyệt mang nguồn gốc nội bộ doOrigin.from(event)trích xuất, trong khi các sự kiệnnew MessageEvent('message', { origin })tổng hợp được tạo trong kiểm thử đơn vị chỉ điền sẵn chuỗievent.originvà gửi trênOrigin.from(event). Việc quay lạiOrigin.from(event.origin)bên trongtry...catchsẽ xử lý cả hai một cách rõ ràng trong khi vẫn từ chối các chuỗi không hợp lệ hoặc"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. Xác thực các đường liên kết và lệnh chuyển hướng giữa các miền con (isSameSite())
Khi xác thực mục tiêu chuyển hướng (chẳng hạn như tham số ?returnTo=) hoặc đường liên kết nội bộ trên các miền con của cùng một miền có thể đăng ký (ví dụ: https://app.example.co.uk và https://billing.example.co.uk:8443), hãy sử dụng isSameSite():
isSameSite()thực hiện so sánh schemeful same-site:https://sub.example.comvàhttp://sub.example.comkhông cùng trang web vì lược đồ của chúng khác nhau.isSameSite()sử dụng Danh sách hậu tố công khai tích hợp sẵn của trình duyệt, vì vậy, các đối tượng thuê riêng biệt trên các hậu tố công khai dùng chung (chẳng hạn nhưhttps://tenant-a.github.iovàhttps://tenant-b.github.io, hoặchttps://a.co.ukvàhttps://b.co.uk) sẽ được đánh giá là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. Phân biệt và ghim nguồn gốc không rõ ràng (origin.opaque và Origin.from(event))
Iframe hộp cát (<iframe sandbox="allow-scripts">), URL data: và new Origin() tạo ra nguồn gốc mờ (origin.opaque === true). Mặc dù quá trình chuyển đổi tuần tự chuỗi của các nguồn gốc này luôn là "null", nhưng Origin.from(event) sẽ trích xuất nguồn gốc mờ thực tế của người gửi từ một MessageEvent do trình duyệt gửi:
- Hai thông báo được gửi từ cùng một iframe hộp cát sẽ tạo ra các đối tượng
OriginisSameOrigin()với nhau (true). - Các thông báo được gửi từ nhiều iframe hộp cát (hoặc các lệnh gọi
Origin.from('data:...')/new Origin()riêng biệt) sẽ tạo ra các nguồn gốc mờ riêng biệt, đánh giáisSameOrigin()thànhfalse, ngăn chặn hành vi giả mạo"null" === "null"trên các bối cảnh mờ không liên quan. new Origin()tạo một nguồn gốc mờ đục mới, duy nhất, không khớp với bất kỳ nguồn gốc nào ngoại trừ chính nó. Đây là một lính gác từ chối tất cả mặc định hữu ích trước khi nguồn gốc ngang hàng dự kiến được ghim một lần khi khởi chạy.
// 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;
}
}
Các phương pháp hay nhất
- NÊN chuyển đổi cả hai bên của phép so sánh thành các thực thể
Originthông quaOrigin.from()trước khi gọiisSameOrigin(other)hoặcisSameSite(other). - NÊN truyền trực tiếp
MessageEventdo trình duyệt gửi đếnOrigin.from(event)khi bạn cần giữ nguyên danh tính nguồn gốc mờ và sử dụngOrigin.from(self)(thay vìLocationhoặcDocument, vốn gửiTypeError) để lấyOrigincủa ngữ cảnh thực thi hiện tại. - NÊN gói các lệnh gọi
Origin.from()trên dữ liệu đầu vào không đáng tin cậy hoặc dữ liệu đầu vào trong thời gian chạy trong một khốitry...catchđể các URL không hợp lệ, chuỗi"null"hoặc đối tượng không được hỗ trợ trả vềfalsemột cách an toàn thay vì gửi mộtTypeErrorkhông phát hiện được. - KHÔNG so sánh trực tiếp hai chuỗi
event.originhoặcurl.origintrong thời gian chạy mà không từ chối"null"(origin !== 'null') trước, vì hai nguồn gốc mờ không liên quan đều chuyển đổi tuần tự thành"null". - KHÔNG xác thực nguồn gốc hoặc trang web bằng
startsWith(),includes(),endsWith()hoặc cắt tên máy chủ.split('.')đơn giản.
Chiến lược dự phòng
Nếu mục tiêu Đường cơ sở của bạn bao gồm các trình duyệt chưa hỗ trợ giao diện Origin, hãy phát hiện tính năng 'Origin' in globalThis và quay lại phân tích cú pháp URL bằng new URL():
- Dự phòng cùng nguồn gốc (nguồn gốc bộ dữ liệu): Trích xuất chuỗi URL (bằng cách sử dụng
event.origincho đối tượngMessageEventhoặccandidate.hrefcho phần tử liên kết), phân tích cú pháp cả URL đề xuất và URL đáng tin cậy bằngnew URL(), từ chối"null"nguồn gốc mờ (candidateUrl.origin !== 'null') và so sánhcandidateUrl.origin === trustedUrl.origin. Vì nguồn gốc mờ không bao giờ có cùng nguồn gốc với nguồn gốc bộ dữ liệu như'https://app.example.com', nên việc từ chối ngay lập tức'null'sẽ khớp vớiOrigin.isSameOrigin(). - Dự phòng cùng trang web (lưu ý): Giao diện
URLkhông hiển thị Danh sách hậu tố công khai của trình duyệt. Trong các trình duyệt không cóOrigin, chỉ thực hiện so khớp hậu tố với một miền có thể đăng ký riêng tư, rõ ràng và đã biết mà bạn kiểm soát (xác minh cảprotocolvà kết quả khớp chính xác hoặc có tiền tố dấu chấmhostname) hoặc sử dụng thư viện Danh sách hậu tố công khai được duy trì. - Dự phòng iframe mờ/hộp cát: Trình duyệt không có
Originkhông hiển thị danh tính nguồn gốc mờ riêng biệt trênMessageEvent(event.originluôn là"null"). Nếu bạn cố ý chấp nhận thông báo từ một<iframe sandbox="allow-scripts">hộp cát đã biết, hãy xác minhevent.source === expectedWidgetWindow(hoặc giao tiếp qua mộtMessageChannelMessagePortchuyên dụng được chuyển đến iframe khi khởi chạy) thay vì so sánhevent.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;
}
}