From c6bdae1cd067adbd352192bbdda996d637946db1 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 15 Sep 2026 18:35:42 +0800 Subject: [PATCH] fix(csp): inherit creator policies in local workers Carry header, meta, and report-only policies into blob/data dedicated, shared, and nested workers while retaining their self origin and report rules. Keep blob URLs linked to their creating environment until worker startup, then clone the policies for the worker lifetime. Preserve nested HTTP worker response policies and cover local worker requests, reporting metadata, and object URL metadata lifetimes. --- moli-file-api/src/blob_store.rs | 77 ++++++++- moli-renderer-v8/src/blob.rs | 27 +++- .../src/content_security_policy.rs | 3 + .../content_security_policy/inheritance.rs | 131 +++++++++++++++ .../context_bootstrap/shared_worker_host.rs | 11 ++ .../worker_host/constructor.rs | 51 +++++- moli-renderer-v8/src/document_runtime.rs | 8 + .../src/document_runtime/lifecycle.rs | 1 + .../src/document_runtime/runtime_core.rs | 2 + .../src/document_runtime/security_policy.rs | 115 ++++++++++++- .../context_host/security_policy.rs | 25 +++ .../src/runtime/page_vm/tests/worker.rs | 82 ++++++++++ .../src/shared_worker_runtime/host_worker.rs | 5 + .../src/shared_worker_runtime/loading.rs | 21 ++- .../global_scope/content_security_policy.rs | 153 +++++++++++++----- .../src/worker/global_scope/fetch.rs | 6 +- .../src/worker/global_scope/mod.rs | 21 +++ moli-renderer-v8/src/worker/mod.rs | 2 +- moli-renderer-v8/src/worker/thread/mod.rs | 117 +++++++------- 19 files changed, 732 insertions(+), 126 deletions(-) create mode 100644 moli-renderer-v8/src/content_security_policy/inheritance.rs 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,