diff --git a/moli-file-api/src/blob_store.rs b/moli-file-api/src/blob_store.rs index b27c34990e..5b386f2efb 100644 --- a/moli-file-api/src/blob_store.rs +++ b/moli-file-api/src/blob_store.rs @@ -41,11 +41,12 @@ impl Default for BlobEntries { } #[derive(Debug)] -struct ObjectUrlState { +struct ObjectUrlState { owner_id: Option, lifetime_id: Option, blob_id: BlobId, access_key: Option, + metadata: Option, } /// Renderer-neutral Blob and object URL backing store. @@ -54,13 +55,15 @@ struct ObjectUrlState { /// counts. The embedding layer owns JS wrappers and calls the retain/release /// hooks from its finalizers. #[derive(Debug)] -pub struct BlobStore { +pub struct BlobStore { blobs: Mutex>, next_blob_id: AtomicU64, - object_urls: Mutex>>, + object_urls: Mutex>>, } -impl Default for BlobStore { +impl Default + for BlobStore +{ fn default() -> Self { Self { blobs: Mutex::default(), @@ -70,7 +73,7 @@ impl Default for BlobStore BlobStore +impl BlobStore where OwnerId: Copy + Eq + Hash, PartitionId: Eq, @@ -200,6 +203,26 @@ where blob_id: BlobId, origin: &str, access_key: Option, + ) -> Option { + self.create_object_url_with_metadata( + owner_id, + lifetime_id, + blob_id, + origin, + access_key, + None, + ) + } + + /// Metadata belongs to the URL's creating environment and shares its lifetime. + pub fn create_object_url_with_metadata( + &self, + owner_id: Option, + lifetime_id: Option, + blob_id: BlobId, + origin: &str, + access_key: Option, + metadata: Option, ) -> Option { self.retain_blob_object_url_ref(blob_id)?; let mut object_urls = self.object_urls.lock(); @@ -216,6 +239,7 @@ where lifetime_id, blob_id, access_key, + metadata, }, ); Some(object_url) @@ -238,7 +262,7 @@ where fn revoke_object_url_if( &self, url: &str, - is_authorized: impl FnOnce(&ObjectUrlState) -> bool, + is_authorized: impl FnOnce(&ObjectUrlState) -> bool, ) -> bool { let state = { let mut object_urls = self.object_urls.lock(); @@ -251,6 +275,14 @@ where true } + pub fn object_url_metadata(&self, url: &str) -> Option + where + Metadata: Clone, + { + let url = url.split_once('#').map_or(url, |(url, _)| url); + self.object_urls.lock().get(url)?.metadata.clone() + } + /// Return object URL bytes and MIME type, excluding its fragment. pub fn object_url_bytes_and_type(&self, url: &str) -> Option<(Vec, String)> { let (bytes, mime_type) = self.object_url_shared_bytes_and_type(url)?; @@ -398,6 +430,39 @@ fn random_uuid() -> String { mod tests { use super::*; + #[test] + fn object_url_metadata_follows_url_lifetime_and_preserves_captured_environment() { + let store = BlobStore::>>::default(); + let blob = store.create_blob(Some(1), None, b"source".to_vec(), String::new()); + let environment = Arc::new(Mutex::new("initial policy".to_owned())); + let weak = Arc::downgrade(&environment); + let url = store + .create_object_url_with_metadata( + Some(2), + Some(9), + blob, + "https://example.test", + None, + Some(environment.clone()), + ) + .unwrap(); + *environment.lock() = "updated policy".to_owned(); + let captured = store.object_url_metadata(&format!("{url}#worker")).unwrap(); + assert_eq!(*captured.lock(), "updated policy"); + drop(environment); + assert_eq!(store.cleanup_object_url_lifetime(2, 9), 1); + assert!(store.object_url_metadata(&url).is_none()); + assert!( + weak.upgrade().is_some(), + "the consumer retains its captured environment" + ); + drop(captured); + assert!( + weak.upgrade().is_none(), + "cleanup must release URL environment metadata" + ); + } + #[test] fn object_url_access_keys_preserve_unauthorized_entries_and_release_authorized_entries() { let store = BlobStore::::default(); diff --git a/moli-renderer-v8/src/blob.rs b/moli-renderer-v8/src/blob.rs index f20dfda805..53c20dd322 100644 --- a/moli-renderer-v8/src/blob.rs +++ b/moli-renderer-v8/src/blob.rs @@ -50,8 +50,12 @@ struct ObjectUrlAccessKey { storage_key: moli_storage_key::MoliStorageKey, } -type RendererBlobStore = - BlobStore; +type RendererBlobStore = BlobStore< + ResourceOwnerId, + RendererStoragePartitionIdentity, + ObjectUrlAccessKey, + crate::content_security_policy::ContentSecurityPolicySource, +>; static BLOB_STORE: OnceLock = OnceLock::new(); @@ -406,7 +410,17 @@ pub(super) fn create_object_url_for_object<'s>( let origin = storage_key.origin().to_owned(); let lifetime_id = native_bridge::current_runtime_observable_context_token(scope) .map(native_bridge::RuntimeObservableContextToken::as_u64); - blob_store().create_object_url_with_lifetime_and_access_key( + let policy_source = if let Some(host_ptr) = + crate::util::context_host_ptr_from_global_bridge(scope) + { + let global = scope.get_current_context().global(scope); + // SAFETY: the Window callback keeps its host alive for this call. + unsafe { &*host_ptr }.local_worker_content_security_policy_source_for_global(scope, global) + } else { + crate::worker::worker_content_security_policy_snapshot(scope) + .map(|policy| Arc::new(parking_lot::RwLock::new(policy))) + }; + blob_store().create_object_url_with_metadata( owner_id, lifetime_id, blob_id, @@ -415,9 +429,16 @@ pub(super) fn create_object_url_for_object<'s>( partition, storage_key, }), + policy_source, ) } +pub(crate) fn object_url_content_security_policy( + url: &str, +) -> Option { + Some(blob_store().object_url_metadata(url)?.read().clone()) +} + pub(super) fn revoke_object_url( scope: &mut v8::PinScope<'_, '_>, url: &str, diff --git a/moli-renderer-v8/src/content_security_policy.rs b/moli-renderer-v8/src/content_security_policy.rs index f93d0e215d..941cad9b1d 100644 --- a/moli-renderer-v8/src/content_security_policy.rs +++ b/moli-renderer-v8/src/content_security_policy.rs @@ -16,6 +16,9 @@ use percent_encoding::percent_decode_str; use serde_json::json; use url::Url; +mod inheritance; +pub(crate) use inheritance::{ContentSecurityPolicySource, InheritedContentSecurityPolicy}; + const CONNECT_SRC: &str = "connect-src"; const CHILD_SRC: &str = "child-src"; const DEFAULT_SRC: &str = "default-src"; diff --git a/moli-renderer-v8/src/content_security_policy/inheritance.rs b/moli-renderer-v8/src/content_security_policy/inheritance.rs new file mode 100644 index 0000000000..9641ce9639 --- /dev/null +++ b/moli-renderer-v8/src/content_security_policy/inheritance.rs @@ -0,0 +1,131 @@ +use super::{ContentSecurityPolicyDisposition, ContentSecurityPolicyReportingEndpoints}; +use std::sync::Arc; +use url::Url; + +/// The source and delivery rules survive cloning a policy into a local Worker. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(crate) struct InheritedContentSecurityPolicy { + pub(crate) self_url: Option, + pub(crate) header_policies: Vec, + pub(crate) meta_policies: Vec, + pub(crate) report_only_policies: Vec, + pub(crate) reporting_endpoints: ContentSecurityPolicyReportingEndpoints, +} + +pub(crate) type ContentSecurityPolicySource = + Arc>; + +impl InheritedContentSecurityPolicy { + pub(crate) fn policies( + &self, + disposition: ContentSecurityPolicyDisposition, + ) -> impl Iterator { + let (headers, meta) = match disposition { + ContentSecurityPolicyDisposition::Enforce => ( + self.header_policies.as_slice(), + self.meta_policies.as_slice(), + ), + ContentSecurityPolicyDisposition::Report => { + (self.report_only_policies.as_slice(), &[][..]) + } + }; + headers + .iter() + .map(|policy| (policy, true)) + .chain(meta.iter().map(|policy| (policy, false))) + } + + pub(crate) fn enforced_strings(&self) -> Vec { + self.policies(ContentSecurityPolicyDisposition::Enforce) + .map(|(policy, _)| policy.to_owned()) + .collect() + } + + pub(crate) fn url_violation( + &self, + protected_url: &Url, + request_url: &Url, + kind: super::ContentSecurityPolicyResourceKind, + redirect_status: super::ContentSecurityPolicyRedirectStatus, + disposition: ContentSecurityPolicyDisposition, + ) -> Option { + self.policies(disposition).find_map(|(policy, report_uri_enabled)| { + let mut violation = super::content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( + std::slice::from_ref(policy), + self.self_url.as_ref().unwrap_or(protected_url), + request_url, + kind, + redirect_status, + disposition, + &self.reporting_endpoints, + )?; + violation.document_uri = super::csp_url_for_report(protected_url); + violation.source_file = super::csp_url_for_report(protected_url); + if !report_uri_enabled { + violation.report_uri_endpoints.clear(); + } + Some(violation) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::content_security_policy::{ + ContentSecurityPolicyRedirectStatus, ContentSecurityPolicyResourceKind, + }; + + #[test] + fn inherited_worker_policy_preserves_self_origin_delivery_and_report_url() { + let source_url = Url::parse("https://example.test/creator/page.html").unwrap(); + let policy_text = "connect-src 'self'; report-uri ./report".to_owned(); + for scheme in ["blob:https://example.test/worker", "data:text/javascript,"] { + let worker_url = Url::parse(scheme).unwrap(); + for disposition in [ + ContentSecurityPolicyDisposition::Enforce, + ContentSecurityPolicyDisposition::Report, + ] { + for is_meta in [false, true] { + if is_meta && disposition == ContentSecurityPolicyDisposition::Report { + continue; + } + let mut policy = InheritedContentSecurityPolicy { + self_url: Some(source_url.clone()), + ..Default::default() + }; + match (disposition, is_meta) { + (ContentSecurityPolicyDisposition::Report, _) => { + policy.report_only_policies.push(policy_text.clone()) + } + (_, true) => policy.meta_policies.push(policy_text.clone()), + (_, false) => policy.header_policies.push(policy_text.clone()), + } + let check = |request: &str| { + policy.url_violation( + &worker_url, + &Url::parse(request).unwrap(), + ContentSecurityPolicyResourceKind::WorkerConnect, + ContentSecurityPolicyRedirectStatus::NoRedirect, + disposition, + ) + }; + assert!(check("https://example.test/allowed").is_none()); + let violation = check("https://cross.test/blocked").unwrap(); + assert_eq!(violation.document_uri, worker_url.scheme()); + assert_eq!(violation.source_file, worker_url.scheme()); + assert_eq!(violation.original_policy, policy_text); + assert_eq!(violation.disposition, disposition); + if is_meta { + assert!(violation.report_uri_endpoints.is_empty()); + } else { + assert_eq!( + violation.report_uri_endpoints, + vec!["https://example.test/creator/report"] + ); + } + } + } + } + } +} diff --git a/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs b/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs index a32c1aa5f8..090532e1c1 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs @@ -331,6 +331,17 @@ fn shared_worker_constructor_callback_inner<'s>( } }; let message_port_registry = context.browser_context_runtime.message_port_registry(); + let script_load = if resolved_url.scheme() == "data" { + let global = scope.get_current_context().global(scope); + // SAFETY: the constructor's Window realm owns this host for the call. + let policy = unsafe { &*host_ptr } + .local_worker_content_security_policy_source_for_global(scope, global) + .map(|source| source.read().clone()) + .unwrap_or_default(); + script_load.with_ready_content_security_policy(policy) + } else { + script_load + }; let Some(message_port_realm) = MessagePortRealmBinding::current(scope) else { throw_type_error( scope, diff --git a/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs b/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs index 5ae842eaeb..1dce445cd6 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs @@ -278,6 +278,10 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( rv.set(worker.into()); return; } + // Capture the creator policy before loading the body so concurrent URL + // revocation cannot leave a loaded script without its policy. + let blob_policy = (resolved_url.scheme() == "blob") + .then(|| crate::blob::object_url_content_security_policy(resolved_url.as_str())); let materialized = match materialize_worker_script_source(&resolved_url) { Ok(source) => source, Err(message) => { @@ -287,6 +291,13 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( }; let creator_secure_context = moli_url::is_potentially_trustworthy_url(&base_url); let worker_id = if let Some(script_source) = materialized { + let inherited_policy = if let Some(policy) = blob_policy { + policy.unwrap_or_default() + } else { + host.local_worker_content_security_policy_source_for_owner(dispatch_scope) + .map(|source| source.read().clone()) + .unwrap_or_default() + }; let request_url = worker_script_resource_url(&resolved_url); let network_response = local_worker_main_script_network_response(&resolved_url, &script_source); @@ -324,6 +335,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( creator_request_client.clone(), ) .with_script_kind(worker_options.worker_type) + .with_content_security_policy_snapshot(inherited_policy) .with_module_credentials_mode(worker_options.credentials_mode) .with_module_static_import_initiator_url(base_url.clone()) .with_module_static_import_content_security_policies(document_content_security_policies) @@ -487,7 +499,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( ); return; } - let (script_url, script_source) = + let (script_url, script_source, content_security_policy) = match materialize_nested_worker_script_source(&resolved_url, &nested_context) { Ok(source) => source, Err(message) => { @@ -523,6 +535,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( script_url, nested_context.loader.request_client().clone(), ) + .with_content_security_policy_snapshot(content_security_policy) .with_script_kind(worker_options.worker_type) .with_module_credentials_mode(worker_options.credentials_mode) .with_module_static_import_initiator_url(nested_context.base_url.clone()) @@ -898,9 +911,23 @@ fn child_context_handle_from_global<'s>( fn materialize_nested_worker_script_source( script_url: &Url, context: &NestedWorkerContext, -) -> Result<(String, String), String> { +) -> Result< + ( + String, + String, + crate::content_security_policy::InheritedContentSecurityPolicy, + ), + String, +> { + let blob_policy = (script_url.scheme() == "blob") + .then(|| crate::blob::object_url_content_security_policy(script_url.as_str())); if let Some(source) = materialize_worker_script_source(script_url)? { - return Ok((script_url.to_string(), source)); + let policy = if let Some(policy) = blob_policy { + policy.unwrap_or_default() + } else { + context.content_security_policy_snapshot.clone() + }; + return Ok((script_url.to_string(), source, policy)); } let loader = context.loader.clone(); let resource_url = worker_script_resource_url(script_url); @@ -928,9 +955,25 @@ fn materialize_nested_worker_script_source( response.body_bytes(), )?; let (head, body) = response.into_text_parts(); + let policy = crate::content_security_policy::InheritedContentSecurityPolicy { + self_url: Some(head.final_url.clone()), + header_policies: crate::content_security_policy::content_security_policy_headers( + &head.headers, + ), + report_only_policies: + crate::content_security_policy::content_security_policy_report_only_headers( + &head.headers, + ), + reporting_endpoints: + crate::content_security_policy::content_security_policy_reporting_endpoints_from_headers( + &head.headers, + &head.final_url, + ), + meta_policies: Vec::new(), + }; let mut final_url = head.final_url; final_url.set_fragment(script_url.fragment()); - Ok((final_url.to_string(), body)) + Ok((final_url.to_string(), body, policy)) } fn worker_script_inherits_parent_service_worker_controller(script_url: &Url) -> bool { diff --git a/moli-renderer-v8/src/document_runtime.rs b/moli-renderer-v8/src/document_runtime.rs index 3f909aa7c7..21447e4df8 100644 --- a/moli-renderer-v8/src/document_runtime.rs +++ b/moli-renderer-v8/src/document_runtime.rs @@ -762,6 +762,14 @@ pub(super) struct DocumentRuntime { bypass_content_security_policy: bool, policy_container: DocumentPolicyContainer, delivered_meta_content_security_policies: RefCell>>, + local_worker_policy_sources: RefCell< + HashMap< + DomHandle, + std::sync::Weak< + parking_lot::RwLock, + >, + >, + >, processed_meta_content_security_policy_handles: RefCell>, document_character_set: String, resource_loader_binding: Option, diff --git a/moli-renderer-v8/src/document_runtime/lifecycle.rs b/moli-renderer-v8/src/document_runtime/lifecycle.rs index c93243c0f0..69f39c6d73 100644 --- a/moli-renderer-v8/src/document_runtime/lifecycle.rs +++ b/moli-renderer-v8/src/document_runtime/lifecycle.rs @@ -295,6 +295,7 @@ impl DocumentRuntime { self.delivered_meta_content_security_policies .get_mut() .clear(); + self.local_worker_policy_sources.get_mut().clear(); self.processed_meta_content_security_policy_handles .get_mut() .clear(); diff --git a/moli-renderer-v8/src/document_runtime/runtime_core.rs b/moli-renderer-v8/src/document_runtime/runtime_core.rs index 4a8be9c365..b471a614d4 100644 --- a/moli-renderer-v8/src/document_runtime/runtime_core.rs +++ b/moli-renderer-v8/src/document_runtime/runtime_core.rs @@ -78,6 +78,7 @@ impl DocumentRuntime { bypass_content_security_policy: false, policy_container: DocumentPolicyContainer::default(), delivered_meta_content_security_policies: RefCell::new(HashMap::new()), + local_worker_policy_sources: RefCell::new(HashMap::new()), processed_meta_content_security_policy_handles: RefCell::new(HashSet::new()), document_character_set: "UTF-8".to_owned(), resource_loader_binding: None, @@ -213,6 +214,7 @@ impl DocumentRuntime { bypass_content_security_policy: _, policy_container: _, delivered_meta_content_security_policies: _, + local_worker_policy_sources: _, processed_meta_content_security_policy_handles: _, document_character_set: _, resource_loader_binding: _, diff --git a/moli-renderer-v8/src/document_runtime/security_policy.rs b/moli-renderer-v8/src/document_runtime/security_policy.rs index 6b9a8540c6..22fed22b9b 100644 --- a/moli-renderer-v8/src/document_runtime/security_policy.rs +++ b/moli-renderer-v8/src/document_runtime/security_policy.rs @@ -82,6 +82,7 @@ pub(crate) enum DocumentNavigationEmbeddingContext<'a> { #[derive(Debug, Clone)] pub(crate) struct DocumentConnectPolicySnapshot { + policy_self_url: Option>, enforce_policies: Vec, report_only_policies: Vec, reporting_endpoints: ContentSecurityPolicyReportingEndpoints, @@ -94,6 +95,7 @@ impl DocumentConnectPolicySnapshot { reporting_endpoints: ContentSecurityPolicyReportingEndpoints, ) -> Self { Self { + policy_self_url: None, enforce_policies: document_response_content_security_policy_strings( &enforce_policies, &reporting_endpoints, @@ -107,28 +109,61 @@ impl DocumentConnectPolicySnapshot { !self.enforce_policies.is_empty() || !self.report_only_policies.is_empty() } + pub(crate) fn from_inherited_policy( + policy: &crate::content_security_policy::InheritedContentSecurityPolicy, + ) -> Self { + let mut snapshot = Self::from_policies( + policy.header_policies.clone(), + policy.report_only_policies.clone(), + policy.reporting_endpoints.clone(), + ); + snapshot.policy_self_url = policy.self_url.clone().map(Box::new); + snapshot + .enforce_policies + .extend(policy.meta_policies.iter().map(|policy_text| { + DocumentContentSecurityPolicyString { + policy: policy_text.clone(), + report_uri_enabled: false, + reporting_endpoints: policy.reporting_endpoints.clone(), + } + })); + snapshot + } + pub(crate) fn check_redirect( &self, document_url: &Url, request_url: &Url, ) -> DocumentContentSecurityPolicyCheck { - DocumentContentSecurityPolicyCheck { + let policy_url = self.policy_self_url.as_deref().unwrap_or(document_url); + let mut result = DocumentContentSecurityPolicyCheck { report_only_violations: document_connect_policy_violations( &self.report_only_policies, &self.reporting_endpoints, - document_url, + policy_url, request_url, ContentSecurityPolicyRedirectStatus::FollowedRedirect, ContentSecurityPolicyDisposition::Report, ), enforced_violations: document_connect_policy_violations_from_document_policies( self.enforce_policies.clone(), - document_url, + policy_url, request_url, ContentSecurityPolicyRedirectStatus::FollowedRedirect, ContentSecurityPolicyDisposition::Enforce, ), + }; + for violation in result + .report_only_violations + .iter_mut() + .chain(&mut result.enforced_violations) + { + violation.document_uri = + crate::content_security_policy::csp_url_for_report(document_url); + violation.source_file = + crate::content_security_policy::csp_url_for_report(document_url); } + result } #[cfg(test)] @@ -150,12 +185,19 @@ impl DocumentConnectPolicySnapshot { ) -> Option { document_url_policy_violation_from_document_policies( self.enforce_policies.clone(), - document_url, + self.policy_self_url.as_deref().unwrap_or(document_url), request_url, ContentSecurityPolicyResourceKind::DocumentConnect, redirect_status, ContentSecurityPolicyDisposition::Enforce, ) + .map(|mut violation| { + violation.document_uri = + crate::content_security_policy::csp_url_for_report(document_url); + violation.source_file = + crate::content_security_policy::csp_url_for_report(document_url); + violation + }) } pub(crate) fn report_only_violation( @@ -167,11 +209,18 @@ impl DocumentConnectPolicySnapshot { document_connect_policy_violation( &self.report_only_policies, &self.reporting_endpoints, - document_url, + self.policy_self_url.as_deref().unwrap_or(document_url), request_url, redirect_status, ContentSecurityPolicyDisposition::Report, ) + .map(|mut violation| { + violation.document_uri = + crate::content_security_policy::csp_url_for_report(document_url); + violation.source_file = + crate::content_security_policy::csp_url_for_report(document_url); + violation + }) } } @@ -899,6 +948,10 @@ impl DocumentRuntime { ); } DocumentConnectPolicySnapshot { + policy_self_url: policy + .content_security_policy_self_url + .clone() + .map(Box::new), enforce_policies: self.document_content_security_policy_strings_for_optional_document( document_handle, &policy.response_content_security_policies, @@ -911,6 +964,50 @@ impl DocumentRuntime { } } + pub(crate) fn local_worker_content_security_policy_source( + &self, + document: Option, + document_url: &Url, + policy: &DocumentPolicyContainer, + ) -> crate::content_security_policy::ContentSecurityPolicySource { + use crate::content_security_policy::InheritedContentSecurityPolicy; + let snapshot = if self.bypass_content_security_policy() { + InheritedContentSecurityPolicy::default() + } else { + InheritedContentSecurityPolicy { + self_url: Some( + policy + .content_security_policy_self_url + .as_ref() + .unwrap_or(document_url) + .clone(), + ), + header_policies: policy.response_content_security_policies.clone(), + meta_policies: document + .map(|handle| self.meta_content_security_policy_strings_for_document(handle)) + .unwrap_or_default(), + report_only_policies: policy + .response_content_security_report_only_policies + .clone(), + reporting_endpoints: policy.content_security_reporting_endpoints.clone(), + } + }; + let mut sources = self.local_worker_policy_sources.borrow_mut(); + if let Some(source) = document + .and_then(|handle| sources.get(&handle)) + .and_then(std::sync::Weak::upgrade) + { + *source.write() = snapshot; + return source; + } + sources.retain(|_, source| source.strong_count() != 0); + let source = std::sync::Arc::new(parking_lot::RwLock::new(snapshot)); + if let Some(document) = document { + sources.insert(document, std::sync::Arc::downgrade(&source)); + } + source + } + pub(crate) fn document_subresource_csp_check( &self, request_url: &Url, @@ -1987,6 +2084,14 @@ impl DocumentRuntime { return; } if let Some(policy) = policy { + if let Some(source) = self + .local_worker_policy_sources + .borrow() + .get(&document_handle) + .and_then(std::sync::Weak::upgrade) + { + source.write().meta_policies.push(policy.clone()); + } self.delivered_meta_content_security_policies .borrow_mut() .entry(document_handle) diff --git a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs index 7baf372113..1c2afc3b41 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs @@ -162,6 +162,31 @@ impl JsContextHost { ) } + pub(crate) fn local_worker_content_security_policy_source_for_owner( + &self, + owner: OwnerDispatchScope, + ) -> Option { + let snapshot = self.owner_document_policy_snapshot(owner)?; + // SAFETY: this host and its DocumentRuntime belong to the same ScriptVm. + Some( + unsafe { &*self.runtime }.local_worker_content_security_policy_source( + snapshot.document_handle, + &snapshot.document_url, + &snapshot.policy_container, + ), + ) + } + + pub(crate) fn local_worker_content_security_policy_source_for_global<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + global: v8::Local<'s, v8::Object>, + ) -> Option { + self.local_worker_content_security_policy_source_for_owner( + policy_owner_dispatch_scope_for_global(scope, global), + ) + } + pub(crate) fn document_permissions_policy_for_owner( &self, owner: OwnerDispatchScope, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs index 664f47d115..c717a005b8 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs @@ -1924,6 +1924,88 @@ async fn drive_window_message_until( anyhow::bail!("{context}; diagnostics={diagnostics}; progress_sources={progress_sources:?}"); } +#[tokio::test] +async fn local_worker_content_security_policy_preserves_self_and_blocks_cross_origin() { + run_page_vm_async_test(async move { + for kind in ["dedicated", "shared", "nested"] { + let shared = kind == "shared"; + for blob in [false, true] { + for meta in [false, true] { + let (base_url, same_server) = spawn_path_response_http_server(vec![( + "/allowed", "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *", "ok".to_owned(), Duration::ZERO, + )]).await; + let (cross_url, mut cross_request, cross_server) = spawn_header_capture_http_server().await; + let mut page_vm = test_page_vm_with_document_url(Url::parse(&format!("{base_url}/page.html")).unwrap()); + if !meta { + page_vm.vm_mut().set_response_content_security_policies(&["connect-src 'self'".to_owned()]); + } + let local_executor = page_vm.local_executor.clone(); + let result = local_executor.run(async move { + let source = format!(r#" + const events = []; + addEventListener('securitypolicyviolation', e => events.push([e.effectiveDirective, e.disposition, e.documentURI])); + async function run(send) {{ + const allowed = await fetch({same}).then(r => r.text()).catch(e => 'fetch-error:' + e.name); + let blocked = 'allowed'; + try {{ await fetch({cross}); }} catch (e) {{ blocked = e.name; }} + setTimeout(() => send({{allowed, blocked, events}}), 50); + }} + {start} + "#, + same = serde_json::to_string(&format!("{base_url}/allowed")).unwrap(), + cross = serde_json::to_string(&format!("{cross_url}/blocked")).unwrap(), + start = if shared { "onconnect = e => run(value => e.ports[0].postMessage(value));" } else { "run(value => postMessage(value));" }, + ); + let source = if kind == "nested" { + format!(r#" + const source = {source}; + const url = {url}; + globalThis.child = new Worker(url); + child.onmessage = e => postMessage(e.data); + child.onerror = e => postMessage({{error: e.message}}); + "#, + source = serde_json::to_string(&source).unwrap(), + url = if blob { "URL.createObjectURL(new Blob([source], {type: 'text/javascript'}))" } else { "'data:text/javascript,' + encodeURIComponent(source)" }, + ) + } else { source }; + page_vm.vm_mut().eval(&format!(r#" + globalThis.__localWorkerResult = null; + const source = {source}; + const url = {url}; + {meta} + globalThis.__localWorker = new {constructor}(url); + {port}.onmessage = e => globalThis.__localWorkerResult = e.data; + __localWorker.onerror = e => globalThis.__localWorkerResult = {{error: e.message}}; + {start} + "#, + source = serde_json::to_string(&source).unwrap(), + url = if blob { "URL.createObjectURL(new Blob([source], {type: 'text/javascript'}))" } else { "'data:text/javascript,' + encodeURIComponent(source)" }, + meta = if meta { "const meta = document.createElement('meta'); meta.httpEquiv = 'Content-Security-Policy'; meta.content = \"connect-src 'self'\"; document.head.append(meta);" } else { "" }, + constructor = if shared { "SharedWorker" } else { "Worker" }, + port = if shared { "__localWorker.port" } else { "__localWorker" }, + start = if shared { "__localWorker.port.start();" } else { "" }, + ))?; + let done = "String(globalThis.__localWorkerResult !== null)"; + if shared { + drive_shared_worker_until_done(&mut page_vm, done, "local SharedWorker CSP result").await?; + } else { + drive_websocket_until_done(&mut page_vm, done, "local Worker CSP result").await?; + } + let result = page_vm.vm_mut().eval("JSON.stringify(__localWorkerResult)")?; + page_vm.vm_mut().eval(if shared { "__localWorker.port.close()" } else { "__localWorker.terminate()" })?; + anyhow::Ok(result) + }).await; + same_server.abort(); + cross_server.abort(); + let result: serde_json::Value = serde_json::from_str(&result.expect("local worker CSP test should finish")).unwrap(); + assert_eq!(result, serde_json::json!({"allowed": "ok", "blocked": "TypeError", "events": [["connect-src", "enforce", if blob { "blob" } else { "data" }]]}), "kind={kind}, blob={blob}, meta={meta}"); + assert!(cross_request.try_recv().is_err(), "CSP must prevent contacting the cross-origin target"); + } + } + } + }).await; +} + #[tokio::test] async fn worker_post_message_flows_through_page_client_event_source() { run_page_vm_async_test(async move { diff --git a/moli-renderer-v8/src/shared_worker_runtime/host_worker.rs b/moli-renderer-v8/src/shared_worker_runtime/host_worker.rs index 092fc6916d..43d6ea95c5 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/host_worker.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/host_worker.rs @@ -60,6 +60,11 @@ impl RendererSharedWorkerHost { .with_creator_storage_key(params.key.storage_key().clone()) .with_indexed_db_manager(execution_policy.indexed_db_manager) .with_storage_bucket_store(execution_policy.storage_bucket_store); + let options = if let Some(policy) = script.content_security_policy_snapshot { + options.with_content_security_policy_snapshot(*policy) + } else { + options + }; let options = if let Some(runtime) = execution_policy.service_worker_runtime { options.with_service_worker_runtime(runtime) } else { diff --git a/moli-renderer-v8/src/shared_worker_runtime/loading.rs b/moli-renderer-v8/src/shared_worker_runtime/loading.rs index b7c1c59bb9..4274d8e593 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/loading.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/loading.rs @@ -35,6 +35,8 @@ pub(crate) struct SharedWorkerScriptLoad { pub(super) struct SharedWorkerLoadedScript { pub(super) script_url: String, pub(super) source: String, + pub(super) content_security_policy_snapshot: + Option>, pub(super) response_referrer_policy: Option, pub(super) response_policy_context: Option, pub(super) response_content_security_policies: Vec, @@ -303,6 +305,7 @@ impl SharedWorkerLoadedScript { Self { script_url, source, + content_security_policy_snapshot: None, response_referrer_policy: None, response_policy_context: None, response_content_security_policies: Vec::new(), @@ -356,6 +359,16 @@ impl SharedWorkerLoadedScript { } impl SharedWorkerScriptLoad { + pub(crate) fn with_ready_content_security_policy( + mut self, + policy: crate::content_security_policy::InheritedContentSecurityPolicy, + ) -> Self { + if let SharedWorkerScriptLoadKind::Ready(script) = &mut self.kind { + script.content_security_policy_snapshot = Some(Box::new(policy)); + } + self + } + pub(crate) fn ready(script_url: String, script_source: String) -> Self { Self { kind: SharedWorkerScriptLoadKind::Ready(SharedWorkerLoadedScript::new( @@ -595,8 +608,14 @@ pub(super) fn load_shared_worker_blob_script_source( ) -> Result { let mut resource_url = script_url.clone(); resource_url.set_fragment(None); + let policy = + crate::blob::object_url_content_security_policy(resource_url.as_str()).unwrap_or_default(); crate::blob::object_url_body_and_type(resource_url.as_str()) - .map(|(body, _)| SharedWorkerLoadedScript::new(script_url.to_string(), body)) + .map(|(body, _)| { + let mut script = SharedWorkerLoadedScript::new(script_url.to_string(), body); + script.content_security_policy_snapshot = Some(Box::new(policy)); + script + }) .ok_or_else(|| { format!("Failed to load shared worker script `{script_url}`: blob URL is unavailable.") }) diff --git a/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs b/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs index 763d7bc245..e4cafc8836 100644 --- a/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs +++ b/moli-renderer-v8/src/worker/global_scope/content_security_policy.rs @@ -12,7 +12,6 @@ use crate::content_security_policy::{ content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints, content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints, content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints, - content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints, create_security_policy_violation_event, current_script_violation_location, send_content_security_policy_reports, }; @@ -39,6 +38,70 @@ use super::{ record_worker_subresource_failure_with_handle, request_body_text, }; +pub(super) fn worker_policy_snapshot( + state: &WorkerGlobalState, +) -> crate::content_security_policy::InheritedContentSecurityPolicy { + state + .content_security_policy_snapshot + .clone() + .unwrap_or_else( + || crate::content_security_policy::InheritedContentSecurityPolicy { + self_url: state.current_script_url.clone(), + header_policies: state.content_security_policies.clone(), + meta_policies: Vec::new(), + report_only_policies: state.content_security_report_only_policies.clone(), + reporting_endpoints: state.content_security_reporting_endpoints.clone(), + }, + ) +} + +fn worker_policy_url<'a>(state: &'a WorkerGlobalState, protected_url: &'a Url) -> &'a Url { + state + .content_security_policy_snapshot + .as_ref() + .and_then(|policy| policy.self_url.as_ref()) + .unwrap_or(protected_url) +} + +fn worker_policy_violation( + protected_url: &Url, + report_uri_enabled: bool, + mut violation: ContentSecurityPolicyUrlViolation, +) -> ContentSecurityPolicyUrlViolation { + violation.document_uri = crate::content_security_policy::csp_url_for_report(protected_url); + violation.source_file = crate::content_security_policy::csp_url_for_report(protected_url); + if !report_uri_enabled { + violation.report_uri_endpoints.clear(); + } + violation +} + +fn worker_policies( + state: &WorkerGlobalState, + disposition: ContentSecurityPolicyDisposition, +) -> impl Iterator { + let (headers, meta) = match (state.content_security_policy_snapshot.as_ref(), disposition) { + (Some(policy), ContentSecurityPolicyDisposition::Enforce) => ( + policy.header_policies.as_slice(), + policy.meta_policies.as_slice(), + ), + (Some(policy), ContentSecurityPolicyDisposition::Report) => { + (policy.report_only_policies.as_slice(), &[][..]) + } + (None, ContentSecurityPolicyDisposition::Enforce) => { + (state.content_security_policies.as_slice(), &[][..]) + } + (None, ContentSecurityPolicyDisposition::Report) => ( + state.content_security_report_only_policies.as_slice(), + &[][..], + ), + }; + headers + .iter() + .map(|policy| (policy, true)) + .chain(meta.iter().map(|policy| (policy, false))) +} + pub(super) fn dispatch_worker_content_security_policy_violation_event<'s>( scope: &mut v8::PinScope<'s, '_>, request_client: &crate::network::context::WorkerResourceLoader, @@ -99,15 +162,15 @@ pub(super) fn dispatch_worker_trusted_types_sink_violation_event_for_state<'s>( return; }; trusted_types_policies(&state_ref) - .filter_map(|(policy, disposition)| { + .filter_map(|(policy, disposition, report_uri_enabled)| { content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints( policy, - protected_url, + worker_policy_url(&state_ref, protected_url), sink, sample, disposition, &state_ref.content_security_reporting_endpoints, - ) + ).map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation)) }) .collect() }; @@ -126,15 +189,15 @@ pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>( return true; }; trusted_types_policies(&state_ref) - .filter_map(|(policy, disposition)| { + .filter_map(|(policy, disposition, report_uri_enabled)| { content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints( policy, - protected_url, + worker_policy_url(&state_ref, protected_url), policy_name, is_duplicate, disposition, &state_ref.content_security_reporting_endpoints, - ) + ).map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation)) }) .collect() }; @@ -147,24 +210,16 @@ pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>( fn trusted_types_policies( state: &WorkerGlobalState, -) -> impl Iterator { - // Preserve the same partition ordering as document reporting. Identical - // policies remain distinct entries and each can produce a violation event. +) -> impl Iterator { [ - ( - &state.content_security_policies, - ContentSecurityPolicyDisposition::Enforce, - ), - ( - &state.content_security_report_only_policies, - ContentSecurityPolicyDisposition::Report, - ), + ContentSecurityPolicyDisposition::Enforce, + ContentSecurityPolicyDisposition::Report, ] .into_iter() - .flat_map(|(policies, disposition)| { - policies - .iter() - .map(move |policy| (policy.as_str(), disposition)) + .flat_map(move |disposition| { + worker_policies(state, disposition).map(move |(policy, report_uri_enabled)| { + (policy.as_str(), disposition, report_uri_enabled) + }) }) } @@ -889,24 +944,44 @@ pub(super) fn worker_eval_content_security_policy_violation( source: Option<&str>, disposition: ContentSecurityPolicyDisposition, ) -> Option { - let policies = match disposition { - ContentSecurityPolicyDisposition::Enforce => &state.content_security_policies, - ContentSecurityPolicyDisposition::Report => &state.content_security_report_only_policies, - }; let kind = if allow_trusted_types_eval { ContentSecurityPolicyNonUrlKind::TrustedTypesEval } else { ContentSecurityPolicyNonUrlKind::Eval }; - policies.iter().find_map(|policy| { + worker_policies(state, disposition).find_map(|(policy, report_uri_enabled)| { content_security_policy_non_url_violation_with_source( policy, - protected_url, + worker_policy_url(state, protected_url), kind, source, disposition, &state.content_security_reporting_endpoints, ) + .map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation)) + }) +} + +fn worker_url_policy_violation( + state: &WorkerGlobalState, + protected_url: &Url, + checked_url: &Url, + blocked_url: &Url, + kind: ContentSecurityPolicyResourceKind, + redirect_status: ContentSecurityPolicyRedirectStatus, + disposition: ContentSecurityPolicyDisposition, +) -> Option { + worker_policies(state, disposition).find_map(|(policy, report_uri_enabled)| { + content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints( + std::slice::from_ref(policy), + worker_policy_url(state, protected_url), + checked_url, + blocked_url, + kind, + redirect_status, + disposition, + &state.content_security_reporting_endpoints, + ).map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation)) }) } @@ -917,14 +992,14 @@ pub(super) fn worker_content_security_policy_violation_with_redirect_status( kind: ContentSecurityPolicyResourceKind, redirect_status: ContentSecurityPolicyRedirectStatus, ) -> Option { - content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( - &state.content_security_policies, + worker_url_policy_violation( + state, protected_url, request_url, + request_url, kind, redirect_status, ContentSecurityPolicyDisposition::Enforce, - &state.content_security_reporting_endpoints, ) } @@ -936,15 +1011,14 @@ pub(super) fn worker_content_security_policy_violation_for_checked_url_with_redi kind: ContentSecurityPolicyResourceKind, redirect_status: ContentSecurityPolicyRedirectStatus, ) -> Option { - content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints( - &state.content_security_policies, + worker_url_policy_violation( + state, protected_url, checked_url, blocked_url, kind, redirect_status, ContentSecurityPolicyDisposition::Enforce, - &state.content_security_reporting_endpoints, ) } @@ -970,14 +1044,14 @@ pub(super) fn worker_content_security_policy_report_only_violation_with_redirect kind: ContentSecurityPolicyResourceKind, redirect_status: ContentSecurityPolicyRedirectStatus, ) -> Option { - content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( - &state.content_security_report_only_policies, + worker_url_policy_violation( + state, protected_url, request_url, + request_url, kind, redirect_status, ContentSecurityPolicyDisposition::Report, - &state.content_security_reporting_endpoints, ) } @@ -989,15 +1063,14 @@ pub(super) fn worker_content_security_policy_report_only_violation_for_checked_u kind: ContentSecurityPolicyResourceKind, redirect_status: ContentSecurityPolicyRedirectStatus, ) -> Option { - content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints( - &state.content_security_report_only_policies, + worker_url_policy_violation( + state, protected_url, checked_url, blocked_url, kind, redirect_status, ContentSecurityPolicyDisposition::Report, - &state.content_security_reporting_endpoints, ) } diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 67347fb6d3..dea095038a 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -2330,10 +2330,8 @@ pub(in crate::worker) fn worker_fetch_callback<'s>( let connect_policy = { let state = state.borrow(); - crate::document_runtime::DocumentConnectPolicySnapshot::from_policies( - state.content_security_policies.clone(), - state.content_security_report_only_policies.clone(), - state.content_security_reporting_endpoints.clone(), + crate::document_runtime::DocumentConnectPolicySnapshot::from_inherited_policy( + &super::content_security_policy::worker_policy_snapshot(&state), ) }; // Local URLs are resolved by the worker fetch task without interception. diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 4b64586a10..7ed47922b2 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -1558,6 +1558,8 @@ pub(crate) struct WorkerGlobalState { pub(super) module_static_import_content_security_policies: Vec, /// Enforce CSP policies parsed from the top-level worker script response. pub(super) content_security_policies: Vec, + pub(super) content_security_policy_snapshot: + Option, /// Report-only CSP policies parsed from the top-level worker script response. pub(super) content_security_report_only_policies: Vec, /// Reporting API endpoints parsed from the top-level worker script response. @@ -2860,6 +2862,8 @@ pub(crate) struct NestedWorkerContext { pub(crate) indexed_db_manager: Option, pub(crate) storage_bucket_store: Option, pub(crate) module_static_import_content_security_policies: Vec, + pub(crate) content_security_policy_snapshot: + crate::content_security_policy::InheritedContentSecurityPolicy, pub(crate) require_trusted_types_for_script: bool, pub(crate) network_policy: super::handle::WorkerNetworkPolicy, pub(crate) policy_context: crate::types::SubresourcePolicyContext, @@ -2893,6 +2897,7 @@ pub(crate) fn reserve_nested_worker_context( indexed_db_manager: state.indexed_db_manager.clone(), storage_bucket_store: state.storage_bucket_store.clone(), module_static_import_content_security_policies: state.content_security_policies.clone(), + content_security_policy_snapshot: content_security_policy::worker_policy_snapshot(&state), require_trusted_types_for_script: crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( &state.content_security_policies, @@ -6350,6 +6355,22 @@ pub(crate) fn worker_storage_partition_identity( ) } +impl WorkerGlobalState { + pub(in crate::worker) fn content_security_policy_snapshot_for_inheritance( + &self, + ) -> crate::content_security_policy::InheritedContentSecurityPolicy { + content_security_policy::worker_policy_snapshot(self) + } +} + +pub(crate) fn worker_content_security_policy_snapshot( + scope: &mut v8::PinScope<'_, '_>, +) -> Option { + Some(content_security_policy::worker_policy_snapshot( + &get_worker_state(scope)?.borrow(), + )) +} + pub(crate) fn worker_current_script_url(scope: &mut v8::PinScope<'_, '_>) -> Option { get_worker_state(scope)?.borrow().current_script_url.clone() } diff --git a/moli-renderer-v8/src/worker/mod.rs b/moli-renderer-v8/src/worker/mod.rs index 50806afe77..a2a54715d8 100644 --- a/moli-renderer-v8/src/worker/mod.rs +++ b/moli-renderer-v8/src/worker/mod.rs @@ -44,7 +44,7 @@ pub(crate) use global_scope::{ worker_allows_trusted_type_policy_name_by_csp, worker_allows_trusted_types_eval, worker_broadcast_channel_registry, worker_broadcast_channel_storage_key, worker_broadcast_channel_wake_sender, worker_broadcast_channel_wrapper, - worker_current_script_url, worker_global_is_closed, + worker_content_security_policy_snapshot, worker_current_script_url, worker_global_is_closed, worker_message_port_event_listener_snapshots, worker_message_port_registry, worker_message_port_wake_sender, worker_message_port_wrapper, worker_notification_permission_state, worker_opfs_directory_iterator_registry, diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index 182e0e44c3..b438228b82 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -21,7 +21,6 @@ use crate::broadcast_channel_runtime::{ use crate::content_security_policy::{ ContentSecurityPolicyDisposition, ContentSecurityPolicyRedirectStatus, ContentSecurityPolicyReportingEndpoints, ContentSecurityPolicyUrlViolation, - content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints, }; use crate::context_bootstrap::flush_one_pending_file_reader; use crate::exception_reporting::{V8ExceptionReport, build_event_handler_exception_report}; @@ -192,6 +191,8 @@ pub(crate) struct WorkerSpawnOptions { pub(crate) content_security_policies: Vec, pub(crate) content_security_report_only_policies: Vec, pub(crate) content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints, + pub(crate) content_security_policy_snapshot: + Option, pub(crate) network_policy: WorkerNetworkPolicy, pub(crate) policy_context: crate::types::SubresourcePolicyContext, pub(crate) worker_context_runtime: RendererWorkerContextRuntime, @@ -319,6 +320,7 @@ impl WorkerSpawnOptions { module_credentials_mode: RequestCredentialsMode::SameOrigin, referrer_policy: None, module_static_import_content_security_policies: Vec::new(), + content_security_policy_snapshot: None, content_security_policies: Vec::new(), content_security_report_only_policies: Vec::new(), content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints::default( @@ -408,6 +410,17 @@ impl WorkerSpawnOptions { self } + pub(crate) fn with_content_security_policy_snapshot( + mut self, + policy: crate::content_security_policy::InheritedContentSecurityPolicy, + ) -> Self { + self.content_security_policies = policy.enforced_strings(); + self.content_security_report_only_policies = policy.report_only_policies.clone(); + self.content_security_reporting_endpoints = policy.reporting_endpoints.clone(); + self.content_security_policy_snapshot = Some(policy); + self + } + pub(crate) fn with_content_security_reporting_endpoints( mut self, endpoints: ContentSecurityPolicyReportingEndpoints, @@ -590,50 +603,37 @@ fn start_worker_module_graph_fetch( loader: WorkerResourceLoader, network_partition_key: Option, module_static_import_content_security_policies: Vec, - worker_global_content_security_policies: Vec, - worker_global_content_security_report_only_policies: Vec, - worker_global_content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints, + worker_global_policy: crate::content_security_policy::InheritedContentSecurityPolicy, completion_tx: mpsc::UnboundedSender, ) { let fetch_id = request.fetch_id(); - let ( - content_security_policies, - content_security_report_only_policies, - content_security_reporting_endpoints, - resource_kind, - ) = - match request.csp_source() { + let (policy, resource_kind) = match request.csp_source() { WorkerModuleGraphFetchCspSource::StaticModuleGraph => ( - module_static_import_content_security_policies, - Vec::new(), - ContentSecurityPolicyReportingEndpoints::default(), + crate::content_security_policy::InheritedContentSecurityPolicy { + self_url: Some(request.initiator_url().clone()), + header_policies: module_static_import_content_security_policies, + ..Default::default() + }, crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerStaticModuleImport, ), WorkerModuleGraphFetchCspSource::DynamicImportGraph => ( - worker_global_content_security_policies, - worker_global_content_security_report_only_policies, - worker_global_content_security_reporting_endpoints, + worker_global_policy, crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport, ), }; - let initial_csp_report_only_violation = - content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( - &content_security_report_only_policies, - request.initiator_url(), - request.url(), - resource_kind, - ContentSecurityPolicyRedirectStatus::NoRedirect, - ContentSecurityPolicyDisposition::Report, - &content_security_reporting_endpoints, - ); - if let Some(violation) = content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( - &content_security_policies, + let initial_csp_report_only_violation = policy.url_violation( + request.initiator_url(), + request.url(), + resource_kind, + ContentSecurityPolicyRedirectStatus::NoRedirect, + ContentSecurityPolicyDisposition::Report, + ); + if let Some(violation) = policy.url_violation( request.initiator_url(), request.url(), resource_kind, ContentSecurityPolicyRedirectStatus::NoRedirect, ContentSecurityPolicyDisposition::Enforce, - &content_security_reporting_endpoints, ) { let message = module_graph_csp_violation_message(&violation); let mut completion = WorkerModuleGraphFetchCompletion::new(fetch_id, Err(message)) @@ -675,11 +675,7 @@ fn start_worker_module_graph_fetch( let requested_module_type = request.module_type().map(str::to_owned); let requested_kind = request.kind(); let request_credentials_mode = request.credentials_mode(); - let response_content_security_policies = content_security_policies.clone(); - let response_content_security_report_only_policies = - content_security_report_only_policies.clone(); - let response_content_security_reporting_endpoints = - content_security_reporting_endpoints.clone(); + let response_policy = policy.clone(); let response_resource_kind = resource_kind; let completion_tx_for_callback = completion_tx.clone(); let response_started_at = Instant::now(); @@ -705,25 +701,20 @@ fn start_worker_module_graph_fetch( if redirect_status == ContentSecurityPolicyRedirectStatus::FollowedRedirect && csp_report_only_violation.is_none() { - csp_report_only_violation = - content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( - &response_content_security_report_only_policies, - &request_initiator_url, - &response.final_url, - response_resource_kind, - redirect_status, - ContentSecurityPolicyDisposition::Report, - &response_content_security_reporting_endpoints, - ); + csp_report_only_violation = response_policy.url_violation( + &request_initiator_url, + &response.final_url, + response_resource_kind, + redirect_status, + ContentSecurityPolicyDisposition::Report, + ); } - if let Some(violation) = content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( - &response_content_security_policies, + if let Some(violation) = response_policy.url_violation( &request_initiator_url, &response.final_url, response_resource_kind, redirect_status, ContentSecurityPolicyDisposition::Enforce, - &response_content_security_reporting_endpoints, ) { let message = module_graph_csp_violation_message(&violation); csp_violation = Some(violation); @@ -809,17 +800,13 @@ fn start_worker_module_graph_fetch( request.url() )), ); - if let Some(violation) = - content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( - &content_security_report_only_policies, - request.initiator_url(), - request.url(), - resource_kind, - ContentSecurityPolicyRedirectStatus::NoRedirect, - ContentSecurityPolicyDisposition::Report, - &content_security_reporting_endpoints, - ) - { + if let Some(violation) = policy.url_violation( + request.initiator_url(), + request.url(), + resource_kind, + ContentSecurityPolicyRedirectStatus::NoRedirect, + ContentSecurityPolicyDisposition::Report, + ) { completion = completion.with_csp_report_only_violation(violation); } let _ = completion_tx.send(completion); @@ -840,9 +827,9 @@ fn start_worker_module_graph_fetch_batch( .borrow() .module_static_import_content_security_policies .clone(), - state.borrow().content_security_policies.clone(), - state.borrow().content_security_report_only_policies.clone(), - state.borrow().content_security_reporting_endpoints.clone(), + state + .borrow() + .content_security_policy_snapshot_for_inheritance(), module_graph_fetch_tx.clone(), ); } @@ -1353,6 +1340,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa content_security_policies, content_security_report_only_policies, content_security_reporting_endpoints, + content_security_policy_snapshot, network_policy, policy_context, worker_context_runtime, @@ -1404,6 +1392,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa content_security_policies, content_security_report_only_policies, content_security_reporting_endpoints, + content_security_policy_snapshot, network_policy, policy_context, worker_context_runtime, @@ -1535,6 +1524,9 @@ async fn worker_main( content_security_policies: Vec, content_security_report_only_policies: Vec, content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints, + content_security_policy_snapshot: Option< + crate::content_security_policy::InheritedContentSecurityPolicy, + >, network_policy: WorkerNetworkPolicy, policy_context: crate::types::SubresourcePolicyContext, worker_context_runtime: RendererWorkerContextRuntime, @@ -1667,6 +1659,7 @@ async fn worker_main( content_security_policies, content_security_report_only_policies, content_security_reporting_endpoints, + content_security_policy_snapshot, secure_context, permission_overrides: network_policy.permission_overrides, extra_http_headers: network_policy.extra_http_headers,