diff --git a/moli-renderer-v8/src/content_security_policy.rs b/moli-renderer-v8/src/content_security_policy.rs index cadfc102d5..cea521c5a5 100644 --- a/moli-renderer-v8/src/content_security_policy.rs +++ b/moli-renderer-v8/src/content_security_policy.rs @@ -44,6 +44,7 @@ pub(crate) enum ContentSecurityPolicyResourceKind { DocumentStyleElement, WorkerConstructor, WorkerConnect, + WorkerDynamicModuleImport, WorkerScript, WorkerStaticModuleImport, } @@ -1910,7 +1911,7 @@ impl ContentSecurityPolicyResourceKind { Self::DocumentImage => IMG_SRC, Self::DocumentManifest => MANIFEST_SRC, Self::DocumentMedia => MEDIA_SRC, - Self::DocumentScriptElement => SCRIPT_SRC_ELEM, + Self::DocumentScriptElement | Self::WorkerDynamicModuleImport => SCRIPT_SRC_ELEM, Self::DocumentStyleElement => STYLE_SRC_ELEM, Self::WorkerConstructor | Self::WorkerStaticModuleImport => WORKER_SRC, Self::WorkerConnect => CONNECT_SRC, @@ -1929,6 +1930,7 @@ impl ContentSecurityPolicyResourceKind { Self::DocumentStyleElement => &[STYLE_SRC_ELEM, STYLE_SRC, DEFAULT_SRC], Self::WorkerConstructor => &[WORKER_SRC, CHILD_SRC, SCRIPT_SRC, DEFAULT_SRC], Self::WorkerConnect => &[CONNECT_SRC, DEFAULT_SRC], + Self::WorkerDynamicModuleImport => &[SCRIPT_SRC_ELEM, SCRIPT_SRC, DEFAULT_SRC], Self::WorkerScript => &[SCRIPT_SRC, DEFAULT_SRC], Self::WorkerStaticModuleImport => &[WORKER_SRC, CHILD_SRC, SCRIPT_SRC, DEFAULT_SRC], } @@ -2389,6 +2391,44 @@ mod tests { )); } + #[test] + fn worker_module_imports_use_their_request_specific_directive_fallbacks() { + let cross_origin = "https://cdn.test/worker-import.js"; + + assert!(!allowed( + "worker-src 'self'; script-src *", + ContentSecurityPolicyResourceKind::WorkerStaticModuleImport, + cross_origin, + )); + assert!(allowed( + "worker-src *; script-src 'self'", + ContentSecurityPolicyResourceKind::WorkerStaticModuleImport, + cross_origin, + )); + assert!(!allowed( + "script-src-elem 'self'; script-src *", + ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport, + cross_origin, + )); + assert!(allowed( + "worker-src 'self'; script-src *", + ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport, + cross_origin, + )); + + let violation = content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints( + &["script-src 'self'".to_owned()], + &protected_url(), + &request_url(cross_origin), + ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport, + ContentSecurityPolicyRedirectStatus::NoRedirect, + ContentSecurityPolicyDisposition::Enforce, + &ContentSecurityPolicyReportingEndpoints::default(), + ) + .expect("script-src fallback should block the cross-origin dynamic import"); + assert_eq!(violation.effective_directive, "script-src-elem"); + } + #[test] fn self_source_uses_csp_secure_upgrade_rules() { let protected = Url::parse("http://app.test/page.html").unwrap(); 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 fb84bc82d9..0f2463b49e 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs @@ -149,7 +149,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( return; }; let global = scope.get_current_context().global(scope); - let document_content_security_policies = + let document_meta_content_security_policies = current_document_content_security_policies(scope, global); let document_referrer_policy = current_document_referrer_policy(scope, global); let host = unsafe { &mut *host_ptr }; @@ -184,6 +184,18 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( crate::native_bridge::WorkerOwnerScope::Top }; let dispatch_scope = crate::native_bridge::OwnerDispatchScope::from(owner_scope); + let mut document_content_security_policies = if let Some(handle) = child_handle { + host.child_browsing_context_content_security_policies(handle) + .map(<[String]>::to_vec) + .unwrap_or_else(|| host.document_content_security_policies().to_vec()) + } else if let Some(policies) = + host.active_lightweight_popup_content_security_policies(scope) + { + policies.to_vec() + } else { + host.document_content_security_policies().to_vec() + }; + document_content_security_policies.extend(document_meta_content_security_policies); let Some(creator_document_loader) = host.document_resource_loader_for_dispatch_scope(dispatch_scope) else { @@ -421,6 +433,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( worker_options.worker_type, worker_options.credentials_mode, document_referrer_policy, + document_content_security_policies, worker_options.name.clone(), reserved_service_worker_client_id, ) diff --git a/moli-renderer-v8/src/native_bridge/context_host/workers.rs b/moli-renderer-v8/src/native_bridge/context_host/workers.rs index e5bc051f50..ef03177ffc 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/workers.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/workers.rs @@ -11,6 +11,11 @@ pub(crate) enum WorkerOwnerScope { LightweightPopup(u64), } +pub(super) struct WorkerModuleStaticImportPolicySnapshot { + pub(super) initiator_url: url::Url, + pub(super) content_security_policies: Vec, +} + pub(super) enum WorkerExecutionState { Loading { pending_messages: Vec, @@ -26,6 +31,7 @@ pub(super) enum WorkerExecutionState { outside_settings_load: crate::network::loads::ResourceLoadLease, name: String, module_credentials_mode: moli_fetch::RequestCredentialsMode, + module_static_import_policy: Option>, storage_key_top_level_site: String, creator_storage_key: MoliStorageKey, reserved_service_worker_client_id: diff --git a/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs b/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs index 4937ea1050..b4665ca55b 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs @@ -1,5 +1,8 @@ use super::super::JsContextHost; -use super::{WorkerConnectionState, WorkerExecutionState, WorkerRelayTerminalState}; +use super::{ + WorkerConnectionState, WorkerExecutionState, WorkerModuleStaticImportPolicySnapshot, + WorkerRelayTerminalState, +}; use crate::RendererSyntheticResponseBody; use crate::network::loads::{ResourceLoadDisposition, ResourceLoadKind, ResourceLoadLease}; use crate::page_task_queue::{ @@ -221,6 +224,7 @@ impl JsContextHost { outside_settings_load, name, module_credentials_mode, + module_static_import_policy: None, storage_key_top_level_site, creator_storage_key, reserved_service_worker_client_id, @@ -240,6 +244,7 @@ impl JsContextHost { script_kind: WorkerScriptKind, module_credentials_mode: moli_fetch::RequestCredentialsMode, document_referrer_policy: Option, + document_content_security_policies: Vec, name: String, reserved_service_worker_client_id: Option, ) -> bool { @@ -265,6 +270,10 @@ impl JsContextHost { let cancel_handle = moli_fetch::FetchCancelHandle::new(); outside_settings_load.attach_cancel_handle(cancel_handle.clone()); let creator_secure_context = moli_url::is_potentially_trustworthy_url(&initiator_url); + let module_static_import_policy = Box::new(WorkerModuleStaticImportPolicySnapshot { + initiator_url: initiator_url.clone(), + content_security_policies: document_content_security_policies, + }); let task_runner = outside_settings_load.task_runner(); let fetch_task_runner = task_runner.clone(); let load_task = task_runner.spawn_abortable(async move { @@ -329,6 +338,7 @@ impl JsContextHost { load_task: slot, terminated, name: loading_name, + module_static_import_policy: loading_module_static_import_policy, .. } => { if *terminated { @@ -336,6 +346,7 @@ impl JsContextHost { return false; } *loading_name = name; + *loading_module_static_import_policy = Some(module_static_import_policy); *slot = Some(load_task); true } @@ -424,18 +435,21 @@ impl JsContextHost { content_security_reporting_endpoints: crate::content_security_policy::ContentSecurityPolicyReportingEndpoints, ) -> bool { + struct FinishLoadingWorkerSpawn { + pending_messages: Vec, + name: String, + storage_key_top_level_site: String, + creator_storage_key: MoliStorageKey, + reserved_service_worker_client_id: Option, + module_credentials_mode: moli_fetch::RequestCredentialsMode, + module_static_import_policy: Option>, + request_client: crate::network::ResourceRequestClient, + } + enum FinishLoadingAction { MissingOrRunning, DiscardTerminated, - Spawn { - pending_messages: Vec, - name: String, - storage_key_top_level_site: String, - creator_storage_key: MoliStorageKey, - reserved_service_worker_client_id: Option, - module_credentials_mode: moli_fetch::RequestCredentialsMode, - request_client: crate::network::ResourceRequestClient, - }, + Spawn(Box), } let action = match self.workers.get_mut(&worker_id) { @@ -446,6 +460,7 @@ impl JsContextHost { terminated, name, module_credentials_mode, + module_static_import_policy, storage_key_top_level_site, creator_storage_key, reserved_service_worker_client_id, @@ -455,7 +470,7 @@ impl JsContextHost { if *terminated { FinishLoadingAction::DiscardTerminated } else { - FinishLoadingAction::Spawn { + FinishLoadingAction::Spawn(Box::new(FinishLoadingWorkerSpawn { pending_messages: std::mem::take(pending_messages), name: name.clone(), storage_key_top_level_site: storage_key_top_level_site.clone(), @@ -463,45 +478,31 @@ impl JsContextHost { reserved_service_worker_client_id: reserved_service_worker_client_id .take(), module_credentials_mode: *module_credentials_mode, + module_static_import_policy: module_static_import_policy.take(), request_client: outside_settings_load.request_client(), - } + })) } } WorkerExecutionState::Running { .. } => FinishLoadingAction::MissingOrRunning, }, None => FinishLoadingAction::MissingOrRunning, }; - let ( + let FinishLoadingWorkerSpawn { pending_messages, name, storage_key_top_level_site, creator_storage_key, reserved_service_worker_client_id, module_credentials_mode, + module_static_import_policy, request_client, - ) = match action { + } = match action { FinishLoadingAction::MissingOrRunning => return false, FinishLoadingAction::DiscardTerminated => { self.forget_worker(worker_id); return false; } - FinishLoadingAction::Spawn { - pending_messages, - name, - storage_key_top_level_site, - creator_storage_key, - reserved_service_worker_client_id, - module_credentials_mode, - request_client, - } => ( - pending_messages, - name, - storage_key_top_level_site, - creator_storage_key, - reserved_service_worker_client_id, - module_credentials_mode, - request_client, - ), + FinishLoadingAction::Spawn(spawn) => *spawn, }; let network_policy = WorkerNetworkPolicy { secure_context, @@ -538,6 +539,13 @@ impl JsContextHost { self.browser_context_runtime() .dedicated_worker_pause_on_start_for_devtools(), ); + if let Some(policy) = module_static_import_policy { + spawn_options = spawn_options + .with_module_static_import_initiator_url(policy.initiator_url) + .with_module_static_import_content_security_policies( + policy.content_security_policies, + ); + } if let Some(client_id) = reserved_service_worker_client_id { spawn_options = spawn_options.with_reserved_service_worker_client_id(client_id); } 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 3cb4fc1114..664f47d115 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs @@ -2911,6 +2911,88 @@ async fn worker_pending_activity_diagnostics_split_loading_and_running_worker_is .await; } +#[tokio::test] +async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports() { + run_page_vm_async_test(async move { + let (dependency_base_url, dependency_server) = spawn_path_response_http_server(vec![( + "/dependency.js", + "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *", + "export const value = 'unexpected';".to_owned(), + Duration::ZERO, + )]) + .await; + let dependency_url = format!("{dependency_base_url}/dependency.js"); + let worker_source = format!( + r#" + import {dependency_url:?}; + postMessage("unexpected"); + "# + ); + let (base_url, server) = spawn_path_response_http_server(vec![( + "/worker.js", + "HTTP/1.1 200 OK", + worker_source, + Duration::ZERO, + )]) + .await; + let document_url = Url::parse(&format!("{base_url}/page.html")).expect("document url"); + let mut page_vm = test_page_vm_with_document_url(document_url.clone()); + page_vm.vm_mut().set_main_navigation_policy_container( + crate::document_runtime::DocumentPolicyContainer::from_navigation_response_headers( + &[( + "Content-Security-Policy".to_owned(), + "worker-src 'self'; script-src data:".to_owned(), + )], + &document_url, + ), + ); + let local_executor = page_vm.local_executor.clone(); + + local_executor + .run(async move { + page_vm.vm_mut().eval( + r#" + (() => { + globalThis.__moduleWorkerCspResult = null; + globalThis.__moduleWorkerCspDone = false; + const worker = new Worker("/worker.js", { type: "module" }); + worker.onmessage = event => { + globalThis.__moduleWorkerCspResult = "message:" + event.data; + globalThis.__moduleWorkerCspDone = true; + }; + worker.onerror = event => { + event.preventDefault(); + globalThis.__moduleWorkerCspResult = "error:" + event.message; + globalThis.__moduleWorkerCspDone = true; + }; + })() + "#, + )?; + drive_websocket_until_done( + &mut page_vm, + "String(globalThis.__moduleWorkerCspDone === true)", + "creator CSP should settle the external module worker", + ) + .await?; + let result = page_vm + .vm_mut() + .eval("globalThis.__moduleWorkerCspResult")?; + assert!( + result.starts_with("error:") && result.contains("Content Security Policy"), + "static module import should be blocked by creator worker-src: {result:?}" + ); + anyhow::Ok(()) + }) + .await + .expect("external module worker creator CSP test should run on owner lane"); + server + .await + .expect("external module worker CSP server should finish"); + dependency_server.abort(); + }) + .await; +} + #[tokio::test] async fn service_worker_register_starts_module_worker_global() { run_page_vm_async_test(async move { diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index 74155d65c3..68a1251dae 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -611,7 +611,7 @@ fn start_worker_module_graph_fetch( worker_global_content_security_policies, worker_global_content_security_report_only_policies, worker_global_content_security_reporting_endpoints, - crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerScript, + crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport, ), }; let initial_csp_report_only_violation = diff --git a/moli-renderer-v8/src/worker/thread/tests/modules.rs b/moli-renderer-v8/src/worker/thread/tests/modules.rs index 56a9f28887..065adac0a5 100644 --- a/moli-renderer-v8/src/worker/thread/tests/modules.rs +++ b/moli-renderer-v8/src/worker/thread/tests/modules.rs @@ -2225,7 +2225,7 @@ async fn worker_dynamic_import_uses_worker_response_csp_not_outside_static_csp() .with_module_static_import_content_security_policies(vec![ "worker-src *; script-src 'self'".to_owned(), ]) - .with_content_security_policies(vec!["script-src 'self'".to_owned()]), + .with_content_security_policies(vec!["script-src-elem 'self'; script-src *".to_owned()]), ); let msg = timeout(TIMEOUT, handle.recv()) @@ -2234,7 +2234,7 @@ async fn worker_dynamic_import_uses_worker_response_csp_not_outside_static_csp() .expect("channel closed"); assert_eq!( expect_post_json(msg), - r#"{"status":"blocked","events":[{"type":"securitypolicyviolation","effectiveDirective":"script-src","violatedDirective":"script-src","blockedURI":"http://127.0.0.1:9/worker/dynamic.js","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src 'self'","disposition":"enforce","instance":true}],"name":"TypeError","csp":true}"# + r#"{"status":"blocked","events":[{"type":"securitypolicyviolation","effectiveDirective":"script-src-elem","violatedDirective":"script-src-elem","blockedURI":"http://127.0.0.1:9/worker/dynamic.js","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src-elem 'self'; script-src *","disposition":"enforce","instance":true}],"name":"TypeError","csp":true}"# ); } @@ -2391,8 +2391,8 @@ async fn shared_worker_dynamic_import_csp_block_dispatches_securitypolicyviolati let matched = false; addEventListener("securitypolicyviolation", event => { matched = event.type === "securitypolicyviolation" && - event.effectiveDirective === "script-src" && - event.violatedDirective === "script-src" && + event.effectiveDirective === "script-src-elem" && + event.violatedDirective === "script-src-elem" && event.blockedURI === "http://127.0.0.1:9/worker/dynamic.js" && event.documentURI === "https://app.test/shared-worker.js" && event.originalPolicy === "script-src 'self'" && @@ -2661,7 +2661,7 @@ async fn worker_dynamic_import_report_only_csp_dispatches_without_blocking() { assert_eq!( expect_post_json(msg), format!( - r#"{{"events":[{{"type":"securitypolicyviolation","effectiveDirective":"script-src","violatedDirective":"script-src","blockedURI":"{dep_url}","documentURI":"{script_url}","originalPolicy":"script-src 'none'","disposition":"report","instance":true}}],"value":42}}"# + r#"{{"events":[{{"type":"securitypolicyviolation","effectiveDirective":"script-src-elem","violatedDirective":"script-src-elem","blockedURI":"{dep_url}","documentURI":"{script_url}","originalPolicy":"script-src 'none'","disposition":"report","instance":true}}],"value":42}}"# ) ); server diff --git a/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-script-response-csp-report-to.html b/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-script-response-csp-report-to.html index ed9ef61943..bb51d29a2a 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-script-response-csp-report-to.html +++ b/moli-wpt-compat/fixtures/wpt/ported/sharedworker/sharedworker-script-response-csp-report-to.html @@ -117,7 +117,7 @@ promise_test(async function () { assert_equals(outcome.value, "dynamic-import-ok"); assert_true( outcome.events.some(event => event.disposition === "report" && - event.effectiveDirective === "script-src" && + event.effectiveDirective === "script-src-elem" && event.instance && event.blockedURI.includes("/report-to-dynamic-dependency.js")), "SharedWorker report-only CSP should dispatch a dynamic import SecurityPolicyViolationEvent", @@ -126,7 +126,7 @@ promise_test(async function () { const match = await wait_for_csp_report(token, report => { return report.type === "csp-violation" && report.body.disposition === "report" && - report.body.effectiveDirective === "script-src" && + report.body.effectiveDirective === "script-src-elem" && report.body.blockedURL.includes("/report-to-dynamic-dependency.js"); }); assert_true( diff --git a/moli-wpt-compat/fixtures/wpt/ported/worker/worker-response-csp-report-to.html b/moli-wpt-compat/fixtures/wpt/ported/worker/worker-response-csp-report-to.html index 276b31d1b8..cad441c68a 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/worker/worker-response-csp-report-to.html +++ b/moli-wpt-compat/fixtures/wpt/ported/worker/worker-response-csp-report-to.html @@ -82,7 +82,7 @@ promise_test(async function () { "dedicated worker report-only CSP should dispatch an XHR SecurityPolicyViolationEvent", ); assert_true( - has_event(outcome.events, "script-src", "worker-response-csp-report-to-dynamic-dependency.js"), + has_event(outcome.events, "script-src-elem", "worker-response-csp-report-to-dynamic-dependency.js"), "dedicated worker report-only CSP should dispatch a dynamic import SecurityPolicyViolationEvent", ); @@ -90,7 +90,7 @@ promise_test(async function () { { directive: "connect-src", blockedText: "target.txt?worker-fetch", label: "fetch" }, { directive: "connect-src", blockedText: "target.txt?worker-xhr", label: "XHR" }, { - directive: "script-src", + directive: "script-src-elem", blockedText: "worker-response-csp-report-to-dynamic-dependency.js", label: "dynamic import", },