fix(fetch): honor sandboxed opaque request origins

This commit is contained in:
ldm0
2026-09-09 06:52:51 +08:00
parent c37568518c
commit a0965fdff7
23 changed files with 457 additions and 78 deletions
+46 -28
View File
@@ -14,9 +14,7 @@ use moli_cookie_jar::{
NetworkCookieRequestContext, SharedBrowserCookieStore, StoredCookieQueryReport,
StoredCookieSetReport, same_site_urls,
};
use moli_url::{
is_potentially_trustworthy_url, origin_ascii_serialization, same_origin, tuple_origin_url,
};
use moli_url::{WebOrigin, is_potentially_trustworthy_url, same_origin};
use moli_url_policy::ensure_http_network_transport_url;
use tracing::debug;
use url::Url;
@@ -418,21 +416,17 @@ fn request_origin_header_value(
if !request_needs_origin_header(request, request_url, redirect_chain) {
return None;
}
let Some(initiator_url) = request.cookie_context.initiator_url.as_ref() else {
let Some(request_origin) = request.request_origin() else {
return Some("null".to_owned());
};
let Some(initiator_origin_url) = tuple_origin_url(initiator_url) else {
if request_origin.is_opaque() {
return Some("null".to_owned());
};
}
Some(
if request_has_redirect_tainted_origin(
initiator_origin_url.as_ref(),
&request.url,
redirect_chain,
) {
if request_has_redirect_tainted_origin(&request_origin, &request.url, redirect_chain) {
"null".to_owned()
} else {
origin_ascii_serialization(initiator_origin_url.as_ref())
request_origin.ascii_serialization().to_owned()
},
)
}
@@ -452,22 +446,18 @@ fn request_needs_origin_header(
return false;
}
let Some(initiator_url) = request.cookie_context.initiator_url.as_ref() else {
let Some(request_origin) = request.request_origin() else {
return true;
};
let Some(initiator_origin_url) = tuple_origin_url(initiator_url) else {
if request_origin.is_opaque() {
return true;
};
!same_origin(initiator_origin_url.as_ref(), request_url)
|| request_has_redirect_tainted_origin(
initiator_origin_url.as_ref(),
&request.url,
redirect_chain,
)
}
!request_origin.same_origin_url(request_url)
|| request_has_redirect_tainted_origin(&request_origin, &request.url, redirect_chain)
}
fn request_has_redirect_tainted_origin(
request_origin_url: &Url,
request_origin: &WebOrigin,
original_request_url: &Url,
redirect_chain: &[RedirectInfo],
) -> bool {
@@ -475,7 +465,7 @@ fn request_has_redirect_tainted_origin(
for redirect in redirect_chain {
let next_url = &redirect.to_url;
if !same_origin(next_url, last_url) && !same_origin(request_origin_url, last_url) {
if !same_origin(next_url, last_url) && !request_origin.same_origin_url(last_url) {
return true;
}
last_url = next_url;
@@ -531,14 +521,18 @@ fn append_browser_storage_access_header(
}
fn request_sec_fetch_site(request: &Request, request_url: &Url) -> String {
let Some(initiator_url) = request.cookie_context.initiator_url.as_ref() else {
let Some(request_origin) = request.request_origin() else {
return "none".to_owned();
};
let initiator_url =
tuple_origin_url(initiator_url).unwrap_or(std::borrow::Cow::Borrowed(initiator_url));
if same_origin(initiator_url.as_ref(), request_url) {
if request_origin.is_opaque() {
return "cross-site".to_owned();
}
let Ok(origin_url) = Url::parse(request_origin.ascii_serialization()) else {
return "cross-site".to_owned();
};
if same_origin(&origin_url, request_url) {
"same-origin".to_owned()
} else if same_site_urls(initiator_url.as_ref(), request_url, true) {
} else if same_site_urls(&origin_url, request_url, true) {
"same-site".to_owned()
} else {
"cross-site".to_owned()
@@ -1361,6 +1355,30 @@ mod tests {
);
}
#[test]
fn opaque_request_origin_is_cross_origin_without_hiding_referrer_url() {
let config = FetchConfig::default();
let request_url = url("https://app.test/data");
let request = Request::new("GET", request_url.as_str(), None, Vec::new())
.unwrap()
.with_initiator_url(&url("https://app.test/sandboxed-frame"))
.with_request_origin(moli_url::WebOrigin::Opaque)
.with_browser_request_metadata(BrowserRequestMetadata::Fetch);
let headers =
outgoing_request_headers_for_url(&config, &request, &request_url, &Vec::new(), None);
assert_eq!(header_value(&headers, "origin").as_deref(), Some("null"));
assert_eq!(
header_value(&headers, "sec-fetch-site").as_deref(),
Some("cross-site")
);
assert_eq!(
header_value(&headers, "referer").as_deref(),
Some("https://app.test/sandboxed-frame")
);
}
#[test]
fn post_navigation_without_an_initiator_serializes_opaque_origin() {
let config = FetchConfig::default();
+27 -5
View File
@@ -9,7 +9,7 @@ use moli_cookie_jar::{
NetworkSiteContextMetadata, NetworkSiteContextTrackMetadata, redirect_types_for_request,
site_context_downgrade_type,
};
use moli_url::same_origin;
use moli_url::{WebOrigin, same_origin};
use url::Url;
use crate::{FetchConfig, network_fetch_result::NetworkObservationRecorder};
@@ -33,6 +33,7 @@ pub struct Request {
pub request_mode: RequestMode,
pub redirect_mode: RequestRedirectMode,
pub credentials_mode: RequestCredentialsMode,
request_origin: Option<WebOrigin>,
network_partition_key: Option<String>,
auth: Option<RequestAuth>,
pub cookie_context: NetworkCookieRequestContext,
@@ -383,6 +384,7 @@ impl Request {
request_mode: RequestMode::Navigate,
redirect_mode: RequestRedirectMode::Follow,
credentials_mode: RequestCredentialsMode::Include,
request_origin: None,
network_partition_key: None,
auth: None,
cookie_context: NetworkCookieRequestContext::top_level_navigation("GET"),
@@ -410,6 +412,7 @@ impl Request {
request_mode: RequestMode::Navigate,
redirect_mode: RequestRedirectMode::Follow,
credentials_mode: RequestCredentialsMode::Include,
request_origin: None,
network_partition_key: None,
auth: None,
cookie_context: NetworkCookieRequestContext::top_level_navigation("GET"),
@@ -458,6 +461,7 @@ impl Request {
request_mode: RequestMode::Cors,
redirect_mode: RequestRedirectMode::Follow,
credentials_mode: RequestCredentialsMode::Include,
request_origin: None,
network_partition_key: None,
auth: None,
cookie_context: NetworkCookieRequestContext::subresource(method),
@@ -655,10 +659,8 @@ impl Request {
RequestCredentialsMode::Include => true,
RequestCredentialsMode::Omit => false,
RequestCredentialsMode::SameOrigin => self
.cookie_context
.initiator_url
.as_ref()
.is_none_or(|initiator_url| same_origin(initiator_url, request_url)),
.request_origin()
.is_none_or(|origin| origin.same_origin_url(request_url)),
}
}
@@ -757,6 +759,26 @@ impl Request {
self
}
/// Override the Fetch request's client origin without changing the
/// initiator URL used for referrer and cookie-site calculations.
pub fn with_request_origin(mut self, request_origin: WebOrigin) -> Self {
self.request_origin = Some(request_origin);
self
}
pub fn request_origin(&self) -> Option<WebOrigin> {
self.request_origin.clone().or_else(|| {
self.cookie_context
.initiator_url
.as_ref()
.map(WebOrigin::from_url)
})
}
pub fn explicit_request_origin(&self) -> Option<&WebOrigin> {
self.request_origin.as_ref()
}
pub fn with_site_for_cookies_url(mut self, site_for_cookies_url: &Url) -> Self {
self.cookie_context = self
.cookie_context
+13
View File
@@ -105,6 +105,19 @@ fn request_credentials_mode_controls_cross_origin_cookie_access() {
assert!(!omit_request.allows_credentials_for_url(&same_origin_url));
}
#[test]
fn opaque_request_origin_disallows_same_origin_credentials_for_same_url_origin() {
let document_url = Url::parse("https://example.com/app/page.html").unwrap();
let request_url = Url::parse("https://example.com/api/data").unwrap();
let request = Request::new("GET", request_url.as_str(), None, vec![])
.unwrap()
.with_initiator_url(&document_url)
.with_request_origin(moli_url::WebOrigin::Opaque)
.with_credentials_mode(RequestCredentialsMode::SameOrigin);
assert!(!request.allows_credentials_for_url(&request_url));
}
#[test]
fn explicit_same_site_override_sets_both_site_context_tracks() {
let context = NetworkCookieRequestContext::subresource("GET")
@@ -151,6 +151,14 @@ impl JsContextHost {
Some((entry.frame_id().to_owned(), document_url))
}
pub(crate) fn child_browsing_context_request_origin(
&self,
handle: DomHandle,
) -> Option<String> {
let document_url = self.child_browsing_context_current_url(handle)?;
self.child_browsing_context_document_origin_for_url(handle, &document_url)
}
pub(crate) fn active_child_subresource_request_scope(
&self,
) -> Option<(DomHandle, String, Url)> {
@@ -562,6 +562,7 @@ impl JsContextHost {
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
request_origin: moli_url::WebOrigin,
credentials_mode: moli_fetch::RequestCredentialsMode,
request_mode: moli_fetch::RequestMode,
network_partition_key: Option<String>,
@@ -597,6 +598,7 @@ impl JsContextHost {
keepalive,
connect_policy,
csp_report_context,
request_origin,
),
),
deferred_request_started: false,
@@ -823,6 +825,7 @@ impl JsContextHost {
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
request_origin: moli_url::WebOrigin,
cancel_handle: Option<moli_fetch::FetchCancelHandle>,
credentials_mode: moli_fetch::RequestCredentialsMode,
request_mode: moli_fetch::RequestMode,
@@ -863,6 +866,7 @@ impl JsContextHost {
keepalive,
connect_policy,
csp_report_context,
request_origin,
),
),
deferred_request_started: defer_request_started,
+12 -5
View File
@@ -135,7 +135,9 @@ pub(crate) use self::request_scope::effective_subresource_policy_context;
pub(in crate::network_host) use self::request_scope::{
XHR_CHILD_CONTEXT_HANDLE_SLOT, active_subresource_network_partition_key,
effective_subresource_referrer_policy, effective_subresource_request_scope,
observe_subresource_request_cookie_report, subresource_request_scope_for_owner,
observe_subresource_request_cookie_report,
observe_subresource_request_cookie_report_for_origin, subresource_request_origin_for_owner,
subresource_request_scope_for_owner,
};
#[cfg(test)]
pub(crate) use self::response::materialize_response_object;
@@ -148,16 +150,21 @@ pub(crate) use self::response::{
build_fetch_response_object_from_subresource_body_for_request_mode,
build_filtered_cached_response_object,
build_navigation_preload_response_object_from_stream_for_request_mode,
cors_preflight_request_headers, filter_cors_exposed_response_headers,
is_cors_policy_failure_message, materialize_response_object_body,
materialize_response_object_body_with_chunk_callback, materialize_response_object_head,
cors_preflight_request_headers_for_origin, filter_cors_exposed_response_headers,
filter_cors_exposed_response_headers_for_origin, is_cors_policy_failure_message,
materialize_response_object_body, materialize_response_object_body_with_chunk_callback,
materialize_response_object_head,
materialize_response_object_head_for_service_worker_respond_with,
materialized_body_bytes_from_value, response_constructor_callback,
validate_cors_preflight_response, validate_cors_response,
validate_cors_preflight_response_for_origin, validate_cors_response,
validate_cors_response_for_origin,
validate_cross_origin_embedder_and_document_isolation_policy,
validate_cross_origin_resource_policy, validate_fetch_response_security_policy,
validate_fetch_response_security_policy_for_origin,
validate_fetch_response_security_policy_with_body,
validate_fetch_response_security_policy_with_body_classified,
validate_fetch_response_security_policy_with_body_classified_for_origin,
validate_fetch_response_security_policy_with_body_for_origin,
};
pub(crate) use self::stylesheet_subresource::{
StylesheetSubresourceFetchStart, start_stylesheet_subresource_fetch,
@@ -324,14 +324,14 @@ fn validate_actual_cors_response_parts(
response_url: &url::Url,
response_headers: &[(String, String)],
) -> Result<(), String> {
let Some(initiator_url) = request.cookie_context.initiator_url.as_ref() else {
let Some(request_origin) = request.request_origin() else {
return Ok(());
};
if request.request_mode == RequestMode::NoCors {
return Ok(());
}
validate_cors_response(
initiator_url,
validate_cors_response_for_origin(
&request_origin,
response_url,
response_headers,
request.credentials_mode,
@@ -420,10 +420,10 @@ async fn run_cors_preflight_if_needed(
preflight_request_headers: &[(String, String)],
preflight_observer: Option<&CorsPreflightNetworkObserver>,
) -> Result<(), String> {
if let Some(initiator_url) = request.cookie_context.initiator_url.clone()
if let Some(request_origin) = request.request_origin()
&& request.request_mode != RequestMode::NoCors
&& let Some(preflight_headers) = cors_preflight_request_headers(
&initiator_url,
&& let Some(preflight_headers) = cors_preflight_request_headers_for_origin(
&request_origin,
&request.url,
&request.method,
preflight_request_headers,
@@ -433,9 +433,12 @@ async fn run_cors_preflight_if_needed(
let mut preflight_request =
Request::new("OPTIONS", request.url.as_str(), None, preflight_headers)
.map_err(|error| format!("cors preflight: failed to build request: {error}"))?
.with_initiator_url(&initiator_url)
.with_credentials_mode(RequestCredentialsMode::SameOrigin)
.with_network_partition_key(request.network_partition_key().map(str::to_owned));
if let Some(initiator_url) = request.cookie_context.initiator_url.as_ref() {
preflight_request = preflight_request.with_initiator_url(initiator_url);
}
preflight_request = preflight_request.with_request_origin(request_origin.clone());
if let Some(metadata) = request.browser_request_metadata() {
preflight_request = preflight_request.with_browser_request_metadata(metadata);
} else {
@@ -475,8 +478,8 @@ async fn run_cors_preflight_if_needed(
preflight_response.final_url
));
}
validate_cors_preflight_response(
&initiator_url,
validate_cors_preflight_response_for_origin(
&request_origin,
&preflight_response.final_url,
&request.method,
preflight_request_headers,
@@ -11,9 +11,10 @@ pub(super) fn record_intercepted_fetch(
resolver: v8::Local<'_, v8::PromiseResolver>,
prepared: PreparedWindowFetchRequest,
) {
let request_cookie_report = observe_subresource_request_cookie_report(
let request_cookie_report = observe_subresource_request_cookie_report_for_origin(
prepared.resource_loader.request_client(),
&prepared.document_url,
&prepared.request_origin,
&prepared.resolved_url,
&prepared.method,
prepared.credentials_mode,
@@ -24,6 +25,7 @@ pub(super) fn record_intercepted_fetch(
prepared.keepalive,
prepared.connect_policy,
prepared.csp_report_context,
prepared.request_origin,
prepared.credentials_mode,
prepared.request_mode,
prepared.network_partition_key,
@@ -203,6 +205,7 @@ pub(super) fn spawn_network_fetch(
)
.map_err(|error| error.to_string())?
.with_initiator_url(&prepared.document_url)
.with_request_origin(prepared.request_origin.clone())
.with_request_mode(prepared.request_mode)
.with_credentials_mode(prepared.credentials_mode)
.with_network_partition_key(prepared.network_partition_key.clone())
@@ -219,9 +222,10 @@ pub(super) fn spawn_network_fetch(
.with_browser_request_metadata(BrowserRequestMetadata::Fetch)
.with_subframe_context(prepared.frame_id.is_some());
let request_cookie_report = observe_subresource_request_cookie_report(
let request_cookie_report = observe_subresource_request_cookie_report_for_origin(
prepared.resource_loader.request_client(),
&prepared.document_url,
&prepared.request_origin,
&prepared.resolved_url,
&prepared.method,
prepared.credentials_mode,
@@ -234,8 +238,8 @@ pub(super) fn spawn_network_fetch(
};
let cancel_handle = FetchCancelHandle::new();
let requires_preflight = prepared.request_mode != moli_fetch::RequestMode::NoCors
&& crate::network_host::cors_preflight_request_headers(
&prepared.document_url,
&& crate::network_host::cors_preflight_request_headers_for_origin(
&prepared.request_origin,
&prepared.resolved_url,
&prepared.method,
&prepared.cors_preflight_request_headers,
@@ -247,6 +251,7 @@ pub(super) fn spawn_network_fetch(
prepared.keepalive,
prepared.connect_policy,
prepared.csp_report_context,
prepared.request_origin.clone(),
Some(cancel_handle.clone()),
prepared.credentials_mode,
prepared.request_mode,
@@ -8,6 +8,7 @@ pub(super) struct PreparedWindowFetchRequest {
pub(super) connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
pub(super) csp_report_context: crate::network_host::WindowCspReportRequestContext,
pub(super) document_url: url::Url,
pub(super) request_origin: moli_url::WebOrigin,
pub(super) network_partition_key: Option<String>,
pub(super) document_referrer_policy: Option<String>,
pub(super) policy_context: crate::types::SubresourcePolicyContext,
@@ -53,6 +54,8 @@ pub(super) fn prepare_window_fetch_request<'s>(
.ok_or_else(|| "fetch: Document resource loader is unavailable".to_owned())?;
let (frame_id, document_url) = subresource_request_scope_for_owner(scope, host, request_scope)
.ok_or_else(|| "fetch: Window execution context owner is retired".to_owned())?;
let request_origin = subresource_request_origin_for_owner(scope, host, request_scope)
.ok_or_else(|| "fetch: Window request origin is unavailable".to_owned())?;
let connect_policy = host
.document_connect_policy_snapshot_for_owner(request_scope)
.ok_or_else(|| "fetch: document policy context is unavailable".to_owned())?;
@@ -78,6 +81,7 @@ pub(super) fn prepare_window_fetch_request<'s>(
connect_policy,
csp_report_context,
document_url,
request_origin,
network_partition_key,
document_referrer_policy,
policy_context,
@@ -26,9 +26,10 @@ pub(super) fn dispatch_service_worker_fetch(
return Ok(None);
}
let request_cookie_report = observe_subresource_request_cookie_report(
let request_cookie_report = observe_subresource_request_cookie_report_for_origin(
prepared.resource_loader.request_client(),
&prepared.document_url,
&prepared.request_origin,
&prepared.resolved_url,
&prepared.method,
prepared.credentials_mode,
@@ -41,8 +42,8 @@ pub(super) fn dispatch_service_worker_fetch(
policy_context: prepared.policy_context,
};
let requires_preflight = prepared.request_mode != moli_fetch::RequestMode::NoCors
&& crate::network_host::cors_preflight_request_headers(
&prepared.document_url,
&& crate::network_host::cors_preflight_request_headers_for_origin(
&prepared.request_origin,
&prepared.resolved_url,
&prepared.method,
&prepared.cors_preflight_request_headers,
@@ -55,6 +56,7 @@ pub(super) fn dispatch_service_worker_fetch(
prepared.keepalive,
prepared.connect_policy.clone(),
prepared.csp_report_context.clone(),
prepared.request_origin.clone(),
Some(cancel_handle.clone()),
prepared.credentials_mode,
prepared.request_mode,
@@ -93,6 +95,7 @@ pub(super) fn dispatch_service_worker_fetch(
referrer_policy: prepared.referrer_policy.clone(),
integrity: prepared.integrity.clone(),
keepalive: prepared.keepalive,
request_origin: Some(prepared.request_origin.clone()),
},
);
let dispatch = ServiceWorkerFetchDispatch {
@@ -9,10 +9,29 @@ pub(in crate::network_host) fn observe_subresource_request_cookie_report(
request_url: &url::Url,
method: &str,
credentials_mode: moli_fetch::RequestCredentialsMode,
) -> Option<moli_cookie_jar::StoredCookieQueryReport> {
observe_subresource_request_cookie_report_for_origin(
loader,
document_url,
&moli_url::WebOrigin::from_url(document_url),
request_url,
method,
credentials_mode,
)
}
pub(in crate::network_host) fn observe_subresource_request_cookie_report_for_origin(
loader: &crate::network::ResourceRequestClient,
document_url: &url::Url,
request_origin: &moli_url::WebOrigin,
request_url: &url::Url,
method: &str,
credentials_mode: moli_fetch::RequestCredentialsMode,
) -> Option<moli_cookie_jar::StoredCookieQueryReport> {
let request = Request::new(method, request_url.as_str(), None, Vec::new())
.ok()?
.with_initiator_url(document_url)
.with_request_origin(request_origin.clone())
.with_credentials_mode(credentials_mode);
if !request.allows_credentials_for_url(request_url) {
return None;
@@ -150,6 +169,28 @@ pub(in crate::network_host) fn subresource_request_scope_for_owner(
}
}
pub(in crate::network_host) fn subresource_request_origin_for_owner(
scope: &mut v8::PinScope<'_, '_>,
host: &JsContextHost,
owner: crate::native_bridge::OwnerDispatchScope,
) -> Option<moli_url::WebOrigin> {
match owner {
crate::native_bridge::OwnerDispatchScope::Top => {
Some(moli_url::WebOrigin::from_url(host.document_url()))
}
crate::native_bridge::OwnerDispatchScope::Child(handle) => host
.child_browsing_context_request_origin(handle)
.map(|origin| moli_url::WebOrigin::from_ascii_serialization(&origin)),
crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => host
.lightweight_popup_origin(popup_id)
.map(|origin| moli_url::WebOrigin::from_ascii_serialization(&origin))
.or_else(|| {
host.lightweight_popup_request_base_url(scope, popup_id)
.map(|document_url| moli_url::WebOrigin::from_url(&document_url))
}),
}
}
pub(in crate::network_host) fn effective_subresource_referrer_policy(
scope: &mut v8::PinScope<'_, '_>,
host: &JsContextHost,
@@ -13,13 +13,17 @@ pub(crate) use self::bindings::response_constructor_callback;
pub(in crate::network_host) use self::bindings::{ParsedResponseInit, parse_response_init};
pub(super) use self::body_methods::install_response_body_methods;
pub(crate) use self::cors::{
FetchResponseSecurityViolation, cors_preflight_request_headers,
filter_cors_exposed_response_headers, is_cors_policy_failure_message,
validate_cors_preflight_response, validate_cors_response,
FetchResponseSecurityViolation, cors_preflight_request_headers_for_origin,
filter_cors_exposed_response_headers, filter_cors_exposed_response_headers_for_origin,
is_cors_policy_failure_message, validate_cors_preflight_response_for_origin,
validate_cors_response, validate_cors_response_for_origin,
validate_cross_origin_embedder_and_document_isolation_policy,
validate_cross_origin_resource_policy, validate_fetch_response_security_policy,
validate_fetch_response_security_policy_for_origin,
validate_fetch_response_security_policy_with_body,
validate_fetch_response_security_policy_with_body_classified,
validate_fetch_response_security_policy_with_body_classified_for_origin,
validate_fetch_response_security_policy_with_body_for_origin,
};
#[cfg(test)]
pub(crate) use self::materialize::materialize_response_object;
@@ -1,6 +1,6 @@
use moli_cookie_jar::same_site_urls;
use moli_fetch::{RequestCredentialsMode, RequestMode};
use moli_url::{origin_ascii_serialization, same_origin};
use moli_url::{WebOrigin, origin_ascii_serialization, same_origin};
use moli_web_mime::{
response_header_value, response_header_values, should_opaque_response_be_blocked_by_orb,
should_opaque_response_be_blocked_by_orb_with_body,
@@ -42,14 +42,28 @@ pub(crate) fn validate_cors_response(
response_headers: &[(String, String)],
credentials_mode: RequestCredentialsMode,
) -> Result<(), String> {
if same_origin(document_url, response_url) {
validate_cors_response_for_origin(
&WebOrigin::from_url(document_url),
response_url,
response_headers,
credentials_mode,
)
}
pub(crate) fn validate_cors_response_for_origin(
request_origin: &WebOrigin,
response_url: &url::Url,
response_headers: &[(String, String)],
credentials_mode: RequestCredentialsMode,
) -> Result<(), String> {
if request_origin.same_origin_url(response_url) {
return Ok(());
}
if !matches!(response_url.scheme(), "http" | "https") {
return Ok(());
}
let origin = origin_ascii_serialization(document_url);
let origin = request_origin.ascii_serialization();
let Some(allow_origin) = response_header_value(response_headers, "access-control-allow-origin")
else {
return Err(format!(
@@ -117,6 +131,34 @@ pub(crate) fn validate_fetch_response_security_policy(
}
}
pub(crate) fn validate_fetch_response_security_policy_for_origin(
document_url: &url::Url,
request_origin: &WebOrigin,
response_url: &url::Url,
response_headers: &[(String, String)],
request_mode: RequestMode,
credentials_mode: RequestCredentialsMode,
policy_context: crate::types::SubresourcePolicyContext,
) -> Result<(), String> {
if request_mode == RequestMode::NoCors {
validate_fetch_response_security_policy(
document_url,
response_url,
response_headers,
request_mode,
credentials_mode,
policy_context,
)
} else {
validate_cors_response_for_origin(
request_origin,
response_url,
response_headers,
credentials_mode,
)
}
}
pub(crate) fn validate_fetch_response_security_policy_with_body(
document_url: &url::Url,
response_url: &url::Url,
@@ -178,6 +220,60 @@ pub(crate) fn validate_fetch_response_security_policy_with_body_classified(
}
}
pub(crate) fn validate_fetch_response_security_policy_with_body_for_origin(
document_url: &url::Url,
request_origin: &WebOrigin,
response_url: &url::Url,
response_headers: &[(String, String)],
response_body: &[u8],
request_mode: RequestMode,
credentials_mode: RequestCredentialsMode,
policy_context: crate::types::SubresourcePolicyContext,
) -> Result<(), String> {
validate_fetch_response_security_policy_with_body_classified_for_origin(
document_url,
request_origin,
response_url,
response_headers,
response_body,
request_mode,
credentials_mode,
policy_context,
)
.map_err(FetchResponseSecurityViolation::into_message)
}
pub(crate) fn validate_fetch_response_security_policy_with_body_classified_for_origin(
document_url: &url::Url,
request_origin: &WebOrigin,
response_url: &url::Url,
response_headers: &[(String, String)],
response_body: &[u8],
request_mode: RequestMode,
credentials_mode: RequestCredentialsMode,
policy_context: crate::types::SubresourcePolicyContext,
) -> Result<(), FetchResponseSecurityViolation> {
if request_mode == RequestMode::NoCors {
validate_fetch_response_security_policy_with_body_classified(
document_url,
response_url,
response_headers,
response_body,
request_mode,
credentials_mode,
policy_context,
)
} else {
validate_cors_response_for_origin(
request_origin,
response_url,
response_headers,
credentials_mode,
)
.map_err(FetchResponseSecurityViolation::Rejected)
}
}
pub(crate) fn validate_opaque_response_blocking(
document_url: &url::Url,
response_url: &url::Url,
@@ -360,13 +456,28 @@ pub(crate) fn validate_cross_origin_resource_policy(
))
}
#[cfg(test)]
pub(crate) fn cors_preflight_request_headers(
document_url: &url::Url,
request_url: &url::Url,
method: &str,
request_headers: &[(String, String)],
) -> Option<Vec<(String, String)>> {
if same_origin(document_url, request_url) {
cors_preflight_request_headers_for_origin(
&WebOrigin::from_url(document_url),
request_url,
method,
request_headers,
)
}
pub(crate) fn cors_preflight_request_headers_for_origin(
request_origin: &WebOrigin,
request_url: &url::Url,
method: &str,
request_headers: &[(String, String)],
) -> Option<Vec<(String, String)>> {
if request_origin.same_origin_url(request_url) {
return None;
}
if !matches!(request_url.scheme(), "http" | "https") {
@@ -392,6 +503,7 @@ pub(crate) fn cors_preflight_request_headers(
Some(headers)
}
#[cfg(test)]
pub(crate) fn validate_cors_preflight_response(
document_url: &url::Url,
response_url: &url::Url,
@@ -399,14 +511,32 @@ pub(crate) fn validate_cors_preflight_response(
request_headers: &[(String, String)],
response_status: u16,
response_headers: &[(String, String)],
) -> Result<(), String> {
validate_cors_preflight_response_for_origin(
&WebOrigin::from_url(document_url),
response_url,
requested_method,
request_headers,
response_status,
response_headers,
)
}
pub(crate) fn validate_cors_preflight_response_for_origin(
request_origin: &WebOrigin,
response_url: &url::Url,
requested_method: &str,
request_headers: &[(String, String)],
response_status: u16,
response_headers: &[(String, String)],
) -> Result<(), String> {
if !(200..300).contains(&response_status) {
return Err(format!(
"CORS preflight failed: response status {response_status}"
));
}
validate_cors_response(
document_url,
validate_cors_response_for_origin(
request_origin,
response_url,
response_headers,
RequestCredentialsMode::SameOrigin,
@@ -464,7 +594,21 @@ pub(crate) fn filter_cors_exposed_response_headers(
response_headers: &[(String, String)],
credentials_mode: RequestCredentialsMode,
) -> Vec<(String, String)> {
if same_origin(document_url, response_url) {
filter_cors_exposed_response_headers_for_origin(
&WebOrigin::from_url(document_url),
response_url,
response_headers,
credentials_mode,
)
}
pub(crate) fn filter_cors_exposed_response_headers_for_origin(
request_origin: &WebOrigin,
response_url: &url::Url,
response_headers: &[(String, String)],
credentials_mode: RequestCredentialsMode,
) -> Vec<(String, String)> {
if request_origin.same_origin_url(response_url) {
return response_headers.to_vec();
}
if !matches!(response_url.scheme(), "http" | "https") {
@@ -590,6 +734,30 @@ mod tests {
);
}
#[test]
fn opaque_request_origin_requires_null_cors_opt_in_for_same_url_origin() {
let response_url = url("https://example.test/data");
assert!(
validate_cors_response_for_origin(
&WebOrigin::Opaque,
&response_url,
&[],
RequestCredentialsMode::SameOrigin,
)
.is_err()
);
assert!(
validate_cors_response_for_origin(
&WebOrigin::Opaque,
&response_url,
&[("Access-Control-Allow-Origin".to_owned(), "null".to_owned(),)],
RequestCredentialsMode::SameOrigin,
)
.is_ok()
);
}
#[test]
fn validate_cors_preflight_response_checks_method_and_headers() {
let request_headers = vec![
@@ -1400,6 +1400,7 @@ impl ScriptVm {
// up the ambient Page loader here would silently rebind policy/backend
// to a newer Document identity.
let loader = pending.load.request_client();
let request_origin = pending.request_origin();
let mut request = moli_fetch::Request::new(
&request_method,
request_url.as_str(),
@@ -1407,6 +1408,7 @@ impl ScriptVm {
request_headers.clone(),
)?
.with_initiator_url(&pending.info.document_url)
.with_request_origin(request_origin)
.with_request_mode(pending.request_mode)
.with_credentials_mode(pending.credentials_mode)
.with_network_partition_key(pending.network_partition_key.clone())
@@ -1668,6 +1670,7 @@ impl ScriptVm {
));
}
let loader = pending_fetch.load.request_client();
let request_origin = pending_fetch.request_origin();
let mut request = moli_fetch::Request::new(
&request_method,
request_url.as_str(),
@@ -1675,6 +1678,7 @@ impl ScriptVm {
original_request_headers.clone(),
)?
.with_initiator_url(&pending_fetch.info.document_url)
.with_request_origin(request_origin)
.with_request_mode(pending_fetch.request_mode)
.with_credentials_mode(pending_fetch.credentials_mode)
.with_auth(auth.into())
@@ -3134,8 +3138,9 @@ impl ScriptVm {
return Err(message);
}
if !skip_fetch_security_validation {
crate::network_host::validate_fetch_response_security_policy_with_body(
crate::network_host::validate_fetch_response_security_policy_with_body_for_origin(
&pending.info.document_url,
&pending.request_origin(),
&response.final_url,
&response.headers,
response.body_bytes(),
@@ -3307,8 +3312,9 @@ impl ScriptVm {
.or_else(|| {
(!skip_fetch_security_validation)
.then(|| {
crate::network_host::validate_fetch_response_security_policy_with_body(
crate::network_host::validate_fetch_response_security_policy_with_body_for_origin(
&pending.info.document_url,
&pending.request_origin(),
&response.final_url,
&response.headers,
response.body_bytes(),
@@ -3591,8 +3597,9 @@ impl ScriptVm {
| SubresourceResourceType::Video
| SubresourceResourceType::Xhr
) {
let validation = crate::network_host::validate_fetch_response_security_policy_with_body_classified(
let validation = crate::network_host::validate_fetch_response_security_policy_with_body_classified_for_origin(
&pending.info.document_url,
&pending.request_origin(),
&response.final_url,
&response.headers,
response.body_bytes(),
@@ -3689,8 +3696,8 @@ impl ScriptVm {
SubresourceResourceType::Fetch | SubresourceResourceType::Xhr
) {
observable_response.headers =
crate::network_host::filter_cors_exposed_response_headers(
&pending.info.document_url,
crate::network_host::filter_cors_exposed_response_headers_for_origin(
&pending.request_origin(),
&observable_response.final_url,
&observable_response.headers,
pending.credentials_mode,
@@ -4387,8 +4394,9 @@ impl ScriptVm {
None
}
.or_else(|| {
crate::network_host::validate_fetch_response_security_policy(
crate::network_host::validate_fetch_response_security_policy_for_origin(
&pending.info.document_url,
&pending.request_origin(),
&started.head.final_url,
&started.head.headers,
pending.request_mode,
@@ -4588,8 +4596,9 @@ impl ScriptVm {
| SubresourceResourceType::Xhr
)
.then(|| {
crate::network_host::validate_fetch_response_security_policy(
crate::network_host::validate_fetch_response_security_policy_for_origin(
&pending.info.document_url,
&pending.request_origin(),
&started.head.final_url,
&started.head.headers,
pending.request_mode,
@@ -4780,8 +4789,8 @@ impl ScriptVm {
pending.info.resource_type,
SubresourceResourceType::Fetch | SubresourceResourceType::Xhr
) {
observable_head.headers = crate::network_host::filter_cors_exposed_response_headers(
&pending.info.document_url,
observable_head.headers = crate::network_host::filter_cors_exposed_response_headers_for_origin(
&pending.request_origin(),
&observable_head.final_url,
&observable_head.headers,
pending.credentials_mode,
@@ -5687,6 +5696,7 @@ impl ScriptVm {
trace_fields,
record_started,
);
let request_origin = streaming.pending.request_origin();
if let PendingSubresourceContinuation::Xhr(xhr) =
streaming.pending.continuation
&& let Some(response_body) = xhr_delivery_body
@@ -5706,8 +5716,8 @@ impl ScriptVm {
let xhr = v8::Local::new(scope, &xhr);
let mut observable_head = streaming.head;
observable_head.headers =
crate::network_host::filter_cors_exposed_response_headers(
&streaming.pending.info.document_url,
crate::network_host::filter_cors_exposed_response_headers_for_origin(
&request_origin,
&observable_head.final_url,
&observable_head.headers,
streaming.pending.credentials_mode,
@@ -167,6 +167,7 @@ fn pending_fetch_continuation<'s>(
dispatch_scope,
)
.expect("test Fetch should capture its CSP report context"),
moli_url::WebOrigin::from_url(host.document_url()),
),
)
}
@@ -305,6 +305,7 @@ fn register_pending_window_fetch_for_test(
keepalive,
connect_policy,
csp_report_context,
moli_url::WebOrigin::from_url(&url),
Some(cancel_handle.clone()),
moli_fetch::RequestCredentialsMode::SameOrigin,
moli_fetch::RequestMode::Cors,
@@ -465,6 +466,7 @@ fn register_pending_window_fetch_with_connect_policy_for_test(
keepalive,
crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(&policy),
csp_report_context,
moli_url::WebOrigin::from_url(&document_url),
Some(cancel_handle.clone()),
moli_fetch::RequestCredentialsMode::SameOrigin,
moli_fetch::RequestMode::Cors,
@@ -1096,6 +1098,14 @@ async fn opaque_child_isolated_world_projects_only_its_own_document() {
.child_browsing_context_has_opaque_origin(child_handle),
"sandbox without allow-same-origin must create an opaque child origin"
);
assert_eq!(
vm._context_host
.borrow()
.child_browsing_context_request_origin(child_handle)
.as_deref(),
Some("null"),
"sandboxed child subresource requests must use an opaque client origin"
);
assert_eq!(
vm.eval("document.getElementById('opaque-isolated-frame').contentDocument === null")
.expect("top opaque contentDocument visibility should evaluate"),
@@ -488,6 +488,7 @@ pub(crate) struct ServiceWorkerFetchRequestMetadata {
pub(crate) referrer_policy: String,
pub(crate) integrity: String,
pub(crate) keepalive: bool,
pub(crate) request_origin: Option<moli_url::WebOrigin>,
}
impl Default for ServiceWorkerFetchRequestMetadata {
@@ -498,6 +499,7 @@ impl Default for ServiceWorkerFetchRequestMetadata {
referrer_policy: String::new(),
integrity: String::new(),
keepalive: false,
request_origin: None,
}
}
}
@@ -520,6 +522,7 @@ pub(crate) fn service_worker_fetch_request_metadata(
.and_then(|metadata| metadata.integrity.clone())
.unwrap_or_default(),
keepalive: false,
request_origin: request.explicit_request_origin().cloned(),
}
}
@@ -56,7 +56,7 @@ fn navigation_preload_request_for_job(
} else {
request.with_top_level_navigation_cookie_context()
};
request
let mut request = request
.with_initiator_url(&job.network_context.document_url)
.with_request_mode(job.request_mode)
.with_credentials_mode(job.credentials_mode)
@@ -70,7 +70,11 @@ fn navigation_preload_request_for_job(
} else {
moli_fetch::BrowserNavigationRequestKind::Navigate
})
.with_page_network_policy()
.with_page_network_policy();
if let Some(request_origin) = job.metadata.request_origin.clone() {
request = request.with_request_origin(request_origin);
}
request
})
.map_err(|error| error.to_string())
}
@@ -100,6 +100,9 @@ fn configure_service_worker_network_fallback_request(
job.network_context.resource_type,
))
.with_subframe_context(job.network_context.frame_id.is_some());
if let Some(request_origin) = job.metadata.request_origin.clone() {
request = request.with_request_origin(request_origin);
}
if service_worker_fetch_is_navigation_request(job) {
request = if job.network_context.frame_id.is_some() {
request.with_subframe_navigation_cookie_context()
+14
View File
@@ -217,6 +217,7 @@ pub(super) struct PendingWindowFetchContinuation {
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
request_origin: moli_url::WebOrigin,
}
enum PendingWindowFetchPromise {
@@ -230,12 +231,14 @@ impl PendingWindowFetchContinuation {
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
request_origin: moli_url::WebOrigin,
) -> Self {
Self {
promise: PendingWindowFetchPromise::Active(resolver),
keepalive,
connect_policy,
csp_report_context,
request_origin,
}
}
@@ -276,6 +279,10 @@ impl PendingWindowFetchContinuation {
pub(super) fn csp_report_context(&self) -> &crate::network_host::WindowCspReportRequestContext {
&self.csp_report_context
}
pub(super) fn request_origin(&self) -> &moli_url::WebOrigin {
&self.request_origin
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
@@ -518,6 +525,13 @@ pub(super) struct PendingSubresourceFetchState {
}
impl PendingSubresourceFetchState {
pub(super) fn request_origin(&self) -> moli_url::WebOrigin {
self.continuation
.window_fetch()
.map(|fetch| fetch.request_origin().clone())
.unwrap_or_else(|| moli_url::WebOrigin::from_url(&self.info.document_url))
}
pub(super) fn detach_keepalive_window_fetch(&mut self) -> bool {
let PendingSubresourceExecutionContext::WindowFetch(context) = &self.execution_context
else {
@@ -1833,6 +1833,7 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
referrer_policy: init.referrer_policy.unwrap_or(inherited.referrer_policy),
integrity: init.integrity.unwrap_or(inherited.integrity),
keepalive: init.keepalive.unwrap_or(inherited.keepalive),
request_origin: None,
};
(
url,
@@ -1872,6 +1873,7 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
referrer_policy: init.referrer_policy.unwrap_or_default(),
integrity: init.integrity.unwrap_or_default(),
keepalive: init.keepalive.unwrap_or(false),
request_origin: None,
};
(
url,
@@ -2624,6 +2624,7 @@ async fn service_worker_fetch_event_request_exposes_destination_metadata() {
referrer_policy: "origin".to_owned(),
integrity: "sha256-test".to_owned(),
keepalive: true,
request_origin: None,
},
};
let completion =
+32 -1
View File
@@ -17,7 +17,7 @@ impl TupleOrigin {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WebOrigin {
Tuple(TupleOrigin),
Opaque,
@@ -39,6 +39,12 @@ impl WebOrigin {
}
}
pub fn from_ascii_serialization(serialized: &str) -> Self {
Url::parse(serialized)
.ok()
.map_or(Self::Opaque, |url| Self::from_url(&url))
}
pub fn is_opaque(&self) -> bool {
matches!(self, Self::Opaque)
}
@@ -63,6 +69,10 @@ impl WebOrigin {
_ => false,
}
}
pub fn same_origin_url(&self, url: &Url) -> bool {
self.same_origin(&Self::from_url(url))
}
}
pub fn tuple_origin_url(url: &Url) -> Option<Cow<'_, Url>> {
@@ -188,6 +198,27 @@ mod tests {
));
}
#[test]
fn web_origin_compares_directly_with_urls() {
let origin = WebOrigin::from_url(&url("https://example.test/document"));
assert!(origin.same_origin_url(&url("https://example.test/resource")));
assert!(!origin.same_origin_url(&url("https://other.test/resource")));
assert!(!WebOrigin::Opaque.same_origin_url(&url("https://example.test/resource")));
}
#[test]
fn web_origin_rehydrates_tuple_serializations_and_keeps_null_opaque() {
assert_eq!(
WebOrigin::from_ascii_serialization("https://example.test:8443"),
WebOrigin::from_url(&url("https://example.test:8443/path"))
);
assert_eq!(
WebOrigin::from_ascii_serialization("null"),
WebOrigin::Opaque
);
}
#[test]
fn blob_url_path_fallback_only_accepts_http_https_and_file_schemes() {
let blob = url("blob:https://example.test/object-1");