fix(fetch): honor sandboxed opaque request origins

This commit is contained in:
ldm0
2026-09-27 19:28:51 +08:00
parent 310f5b5024
commit 41672b2d3f
8 changed files with 129 additions and 4 deletions
+23
View File
@@ -1149,6 +1149,29 @@ 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, 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();
+12 -2
View File
@@ -28,7 +28,8 @@ impl<'a> FetchUrlList<'a> {
}
/// Returning to the initiating origin cannot restore basic response tainting.
pub fn has_cross_origin_url(self, origin: &WebOrigin) -> bool {
pub fn has_cross_origin_url(self, origin: impl Into<WebOrigin>) -> bool {
let origin = origin.into();
self.urls().any(|url| !origin.same_origin(&url.into()))
}
@@ -52,13 +53,22 @@ impl<'a> FetchUrlList<'a> {
Ok(())
}
pub fn has_cross_origin_url_for_origin(self, origin: &WebOrigin) -> bool {
self.has_cross_origin_url(origin)
}
pub fn serialized_origin_for_origin(self, origin: &WebOrigin) -> String {
self.serialized_origin(origin)
}
pub fn has_cross_site_url(self, origin: &Url) -> bool {
self.urls().any(|url| !same_site_urls(origin, url, true))
}
/// A first hop out of the initiating origin retains that origin. Crossing
/// origins from an already cross-origin URL serializes the origin as null.
pub fn serialized_origin(self, origin: &WebOrigin) -> String {
pub fn serialized_origin(self, origin: impl Into<WebOrigin>) -> String {
let origin = origin.into();
if self.redirects.iter().any(|redirect| {
!same_origin(&redirect.from_url, &redirect.to_url)
&& !origin.same_origin(&(&redirect.from_url).into())
+13
View File
@@ -110,6 +110,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")
@@ -659,6 +659,34 @@ mod tests {
);
}
#[test]
fn opaque_request_origin_requires_null_cors_opt_in_for_same_url_origin() {
let response_url = url("https://example.test/data");
let response = header_response(response_url.clone(), Vec::new());
assert!(
validate_cors_response_chain(
&WebOrigin::Opaque,
&response,
RequestCredentialsMode::SameOrigin,
)
.is_err()
);
let allowed_response = header_response(
response_url,
vec![("Access-Control-Allow-Origin".to_owned(), "null".to_owned())],
);
assert!(
validate_cors_response_chain(
&WebOrigin::Opaque,
&allowed_response,
RequestCredentialsMode::SameOrigin,
)
.is_ok()
);
}
#[test]
fn validate_cors_preflight_response_checks_method_and_headers() {
let request_headers = vec![
@@ -5857,6 +5857,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
@@ -5881,7 +5882,7 @@ impl ScriptVm {
{
observable_head.headers =
crate::network_host::filter_cors_exposed_response_headers(
&streaming.pending.request_origin,
&request_origin,
&observable_head,
streaming.pending.credentials_mode,
);
@@ -71,6 +71,21 @@ 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"
);
let child_request_origin = {
let host = vm._context_host.borrow();
let owner = crate::native_bridge::OwnerDispatchScope::Child(child_handle);
let loader = host
.document_resource_loader_for_dispatch_scope(owner)
.expect("opaque child resource loader should exist");
host.subresource_request_environment(&loader, owner)
.expect("opaque child request environment should exist")
.request_origin
};
assert_eq!(
child_request_origin,
moli_url::WebOrigin::Opaque,
"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"),
+4
View File
@@ -521,6 +521,10 @@ pub(super) struct PendingSubresourceFetchState {
}
impl PendingSubresourceFetchState {
pub(super) fn request_origin(&self) -> moli_url::WebOrigin {
self.request_origin.clone()
}
pub(super) fn detach_keepalive_window_fetch(&mut self) -> bool {
let PendingSubresourceExecutionContext::WindowFetch(context) = &self.execution_context
else {
+32 -1
View File
@@ -17,7 +17,7 @@ impl TupleOrigin {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WebOrigin {
Tuple(TupleOrigin),
Opaque,
@@ -45,6 +45,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)
}
@@ -69,6 +75,10 @@ impl WebOrigin {
_ => false,
}
}
pub fn same_origin_url(&self, url: &Url) -> bool {
self.same_origin(&Self::from_url(url))
}
}
impl From<&Url> for WebOrigin {
@@ -206,6 +216,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");