diff --git a/moli-fetch/src/blocking/mod.rs b/moli-fetch/src/blocking/mod.rs index be89f76c24..8f98413de0 100644 --- a/moli-fetch/src/blocking/mod.rs +++ b/moli-fetch/src/blocking/mod.rs @@ -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(); diff --git a/moli-fetch/src/request.rs b/moli-fetch/src/request.rs index 998724a5e4..c9217779e6 100644 --- a/moli-fetch/src/request.rs +++ b/moli-fetch/src/request.rs @@ -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, network_partition_key: Option, auth: Option, 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 { + 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 diff --git a/moli-fetch/src/tests/cookie_context.rs b/moli-fetch/src/tests/cookie_context.rs index 3c880332d9..337874613d 100644 --- a/moli-fetch/src/tests/cookie_context.rs +++ b/moli-fetch/src/tests/cookie_context.rs @@ -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") diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs index 971fbb50d3..5a2f87883e 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs @@ -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 { + 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)> { diff --git a/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs b/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs index a47020801b..bd79626b2d 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs @@ -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, @@ -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, 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, diff --git a/moli-renderer-v8/src/network_host.rs b/moli-renderer-v8/src/network_host.rs index 4f1f4188c8..0f73f6a613 100644 --- a/moli-renderer-v8/src/network_host.rs +++ b/moli-renderer-v8/src/network_host.rs @@ -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, diff --git a/moli-renderer-v8/src/network_host/async_fetch.rs b/moli-renderer-v8/src/network_host/async_fetch.rs index 814757e1f7..9caa78798e 100644 --- a/moli-renderer-v8/src/network_host/async_fetch.rs +++ b/moli-renderer-v8/src/network_host/async_fetch.rs @@ -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, diff --git a/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs b/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs index ebca320f8a..30d5a716d0 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs @@ -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, diff --git a/moli-renderer-v8/src/network_host/fetch/bindings/request.rs b/moli-renderer-v8/src/network_host/fetch/bindings/request.rs index c050d8c8be..f5d9e04fd7 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/request.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/request.rs @@ -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, pub(super) document_referrer_policy: Option, 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, diff --git a/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs b/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs index 01e0ea95e4..c5a71120bd 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs @@ -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 { diff --git a/moli-renderer-v8/src/network_host/request_scope.rs b/moli-renderer-v8/src/network_host/request_scope.rs index 65a5e07d7b..e9e24785df 100644 --- a/moli-renderer-v8/src/network_host/request_scope.rs +++ b/moli-renderer-v8/src/network_host/request_scope.rs @@ -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 { + 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 { 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 { + 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, diff --git a/moli-renderer-v8/src/network_host/response.rs b/moli-renderer-v8/src/network_host/response.rs index dc460b9ae0..3c7c36daa2 100644 --- a/moli-renderer-v8/src/network_host/response.rs +++ b/moli-renderer-v8/src/network_host/response.rs @@ -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; diff --git a/moli-renderer-v8/src/network_host/response/cors.rs b/moli-renderer-v8/src/network_host/response/cors.rs index cec63bfc84..e091e172ed 100644 --- a/moli-renderer-v8/src/network_host/response/cors.rs +++ b/moli-renderer-v8/src/network_host/response/cors.rs @@ -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> { - 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> { + 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![ diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch.rs index 32a242ec60..cdb81b9bb5 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs @@ -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, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index c089f73cc9..3fc2975643 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs @@ -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()), ), ) } diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 0bdbe4bac7..6606c2ba2c 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -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"), diff --git a/moli-renderer-v8/src/service_worker_runtime/events.rs b/moli-renderer-v8/src/service_worker_runtime/events.rs index f59c90dd80..a39e4faf5a 100644 --- a/moli-renderer-v8/src/service_worker_runtime/events.rs +++ b/moli-renderer-v8/src/service_worker_runtime/events.rs @@ -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, } 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(), } } diff --git a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs index df5b88ac20..6adce27b59 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs @@ -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()) } diff --git a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs index c84ec3c3d6..1816cf19c2 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs @@ -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() diff --git a/moli-renderer-v8/src/types.rs b/moli-renderer-v8/src/types.rs index 062e16e94d..f6d7f4a0ae 100644 --- a/moli-renderer-v8/src/types.rs +++ b/moli-renderer-v8/src/types.rs @@ -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 { diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 2d90ffaa4e..edc156cfba 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -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, diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index ef41334c94..e507d1fc7d 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -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 = diff --git a/moli-url/src/origin.rs b/moli-url/src/origin.rs index 0820f7d524..8b5966f278 100644 --- a/moli-url/src/origin.rs +++ b/moli-url/src/origin.rs @@ -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> { @@ -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");