mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 00:01:34 +00:00
fix(fetch): honor sandboxed opaque request origins
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user