diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 99a68098f2..940e1808c3 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -17,7 +17,6 @@ content-security-policy/generic/policy-inherited-correctly-by-plznavigate.html content-security-policy/inheritance/document-write-iframe.html content-security-policy/inheritance/location-reload.html content-security-policy/inside-worker/dedicatedworker-report-only.html -content-security-policy/inside-worker/dedicatedworker-worker-src.html content-security-policy/inside-worker/serviceworker-report-only.https.sub.html content-security-policy/navigation/javascript-url-navigation-evaluated-to-string-inherits-csp.html content-security-policy/navigation/to-javascript-parent-initiated-parent-csp.html @@ -30,7 +29,6 @@ content-security-policy/resource-hints/prefetch-no-csp.html content-security-policy/sandbox/autoplay-disabled-by-csp.html content-security-policy/sandbox/window-reuse-sandboxed.html content-security-policy/script-src/non-nonceable-elements.html -content-security-policy/script-src/script-src-trusted_types_eval_DedicatedWorker.html content-security-policy/securitypolicyviolation/blockeduri-inline.html content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html content-security-policy/webrtc/webrtc-allowed-default-src-none.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 58a59fd634..81912a205a 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -169,6 +169,7 @@ content-security-policy/img-src/img-src-none-blocks.html content-security-policy/img-src/img-src-targeting.html content-security-policy/img-src/img-src-wildcard-allowed.html content-security-policy/inheritance/blob-inherits-from-meta-http-equiv-with-invalid-characters.html +content-security-policy/inside-worker/dedicatedworker-worker-src.html content-security-policy/media-src/media-src-7_1.html content-security-policy/media-src/media-src-7_2.html content-security-policy/media-src/media-src-blocked-blob-url.html @@ -236,6 +237,7 @@ content-security-policy/script-src/script-src-strict_dynamic_new_function.html content-security-policy/script-src/script-src-strict_dynamic_non_parser_inserted.html content-security-policy/script-src/script-src-strict_dynamic_non_parser_inserted_incorrect_nonce.html content-security-policy/script-src/script-src-strict_dynamic_parser_inserted_correct_nonce.html +content-security-policy/script-src/script-src-trusted_types_eval_DedicatedWorker.html content-security-policy/script-src/script-src-trusted_types_eval_with_report_only_require_trusted_types_eval.html content-security-policy/script-src/script-src-trusted_types_eval_with_require_trusted_types_eval.html content-security-policy/script-src/script-src-trusted_types_eval_without_require_trusted_types_eval.html diff --git a/moli-renderer-v8/src/content_security_policy.rs b/moli-renderer-v8/src/content_security_policy.rs index 863a2104be..a7760882b2 100644 --- a/moli-renderer-v8/src/content_security_policy.rs +++ b/moli-renderer-v8/src/content_security_policy.rs @@ -43,7 +43,7 @@ pub(crate) enum ContentSecurityPolicyResourceKind { DocumentMedia, DocumentScriptElement, DocumentStyleElement, - SharedWorkerScript, + WorkerConstructor, WorkerConnect, WorkerDynamicModuleImport, WorkerScript, @@ -1976,7 +1976,7 @@ impl ContentSecurityPolicyResourceKind { Self::DocumentMedia => MEDIA_SRC, Self::DocumentScriptElement | Self::WorkerDynamicModuleImport => SCRIPT_SRC_ELEM, Self::DocumentStyleElement => STYLE_SRC_ELEM, - Self::SharedWorkerScript | Self::WorkerStaticModuleImport => WORKER_SRC, + Self::WorkerConstructor | Self::WorkerStaticModuleImport => WORKER_SRC, Self::WorkerConnect => CONNECT_SRC, Self::WorkerScript => SCRIPT_SRC, } @@ -1991,7 +1991,7 @@ impl ContentSecurityPolicyResourceKind { Self::DocumentMedia => &[MEDIA_SRC, DEFAULT_SRC], Self::DocumentScriptElement => &[SCRIPT_SRC_ELEM, SCRIPT_SRC, DEFAULT_SRC], Self::DocumentStyleElement => &[STYLE_SRC_ELEM, STYLE_SRC, DEFAULT_SRC], - Self::SharedWorkerScript => &[WORKER_SRC, CHILD_SRC, SCRIPT_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], @@ -2608,43 +2608,43 @@ mod tests { } #[test] - fn worker_src_none_blocks_shared_worker_script() { + fn worker_src_none_blocks_worker_constructor() { assert!(!allowed( "worker-src 'none'; script-src 'self'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://app.test/worker.js" )); } #[test] - fn shared_worker_script_uses_script_src_and_default_src_fallbacks() { + fn worker_constructor_uses_script_src_and_default_src_fallbacks() { assert!(!allowed( "script-src 'none'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://app.test/worker.js" )); assert!(!allowed( "default-src 'none'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://app.test/worker.js" )); assert!(allowed( "default-src 'self'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://app.test/worker.js" )); } #[test] - fn shared_worker_script_uses_child_src_before_script_src_fallback() { + fn worker_constructor_uses_child_src_before_script_src_fallback() { assert!(!allowed( "child-src 'none'; script-src 'self'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://app.test/worker.js" )); assert!(allowed( "child-src https://workers.test; script-src 'none'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://workers.test/worker.js" )); } @@ -2669,15 +2669,15 @@ mod tests { } #[test] - fn worker_src_takes_precedence_for_shared_worker_scripts() { + fn worker_src_takes_precedence_for_worker_constructors() { assert!(allowed( "default-src 'none'; script-src 'none'; worker-src 'self'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://app.test/worker.js" )); assert!(!allowed( "default-src *; script-src *; worker-src 'none'", - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, "https://app.test/worker.js" )); } diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index d8e5a3a142..5ebde32a21 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -446,7 +446,6 @@ pub(crate) use self::trusted_types::{ install_trusted_types_runtime_state, trusted_html_string_or_throw, trusted_html_value_string, trusted_script_string_for_script_element_execution, trusted_script_string_or_type_error, trusted_script_url_string_or_throw, trusted_types_code_generation_check, - trusted_types_code_generation_check_callback, }; pub(crate) use self::url_search_params_runtime::url_search_params_request_body; pub(crate) use self::web_storage::install_storage_aliases_for_window; diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs index 298fcfe32c..b2dc9cc59f 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs @@ -437,33 +437,6 @@ pub(crate) enum TrustedTypesCodeGenerationCheck { Block, } -pub(crate) fn trusted_types_code_generation_check_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - source: v8::Local<'s, v8::Value>, - is_code_like: bool, -) -> v8::ModifyCodeGenerationFromStringsResult<'s> { - if trusted_types_eval_is_allowed(scope) && source.is_string() { - return v8::ModifyCodeGenerationFromStringsResult { - codegen_allowed: true, - modified_source: None, - }; - } - let requirements = trusted_types_for_script_requirements(scope); - let (codegen_allowed, modified_source) = - match trusted_types_code_generation_check(scope, source, is_code_like, requirements) { - TrustedTypesCodeGenerationCheck::AllowOriginal => (true, None), - TrustedTypesCodeGenerationCheck::AllowModified(source) => { - let source = v8_string(scope, &source); - (source.is_some(), source) - } - TrustedTypesCodeGenerationCheck::Block => (false, None), - }; - v8::ModifyCodeGenerationFromStringsResult { - codegen_allowed, - modified_source, - } -} - pub(crate) fn trusted_types_code_generation_check<'s>( scope: &mut v8::PinScope<'s, '_>, source: v8::Local<'s, v8::Value>, @@ -1413,18 +1386,6 @@ fn trusted_types_for_script_is_required(scope: &mut v8::PinScope<'_, '_>) -> boo unsafe { &*host_ptr }.requires_trusted_types_for_script(scope) } -fn trusted_types_for_script_requirements( - scope: &mut v8::PinScope<'_, '_>, -) -> TrustedTypesForScriptRequirements { - if let Some(required) = crate::worker::worker_requires_trusted_types_for_script(scope) { - return TrustedTypesForScriptRequirements::enforced_only(required); - } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - return TrustedTypesForScriptRequirements::default(); - }; - unsafe { &*host_ptr }.trusted_types_for_script_requirements(scope) -} - fn trusted_types_eval_is_allowed(scope: &mut v8::PinScope<'_, '_>) -> bool { if let Some(allowed) = crate::worker::worker_allows_trusted_types_eval(scope) { return allowed; 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 81e5f9ca00..9d69811b99 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/constructor.rs @@ -475,29 +475,28 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>( return; } }; + if let Err(message) = + crate::worker::check_and_queue_nested_worker_constructor_csp(scope, &resolved_url) + { + queue_nested_worker_script_load_error( + scope, + worker, + &nested_context, + &resolved_url, + message, + ); + return; + } let (script_url, script_source) = match materialize_nested_worker_script_source(&resolved_url, &nested_context) { Ok(source) => source, Err(message) => { - let _ = nested_context.wake_tx.send( - crate::worker::WorkerMessage::NestedWorkerEvent { - worker_id: nested_context.worker_id, - message: Box::new(crate::worker::WorkerToParentMessage::Error { - message, - filename: resolved_url.to_string(), - lineno: 0, - colno: 0, - event_kind: crate::worker::WorkerParentErrorEventKind::Event, - phase: crate::worker::WorkerErrorPhase::Bootstrap, - source: crate::worker::WorkerErrorSource::Runtime, - }), - }, - ); - set_private_value( + queue_nested_worker_script_load_error( scope, worker, - WORKER_ID_SLOT, - v8::Number::new(scope, nested_context.worker_id.as_u64() as f64).into(), + &nested_context, + &resolved_url, + message, ); return; } @@ -816,6 +815,35 @@ pub(in crate::context_bootstrap) fn document_query_encoding_override( .filter(|encoding| *encoding != encoding_rs::UTF_8) } +fn queue_nested_worker_script_load_error( + scope: &mut v8::PinScope<'_, '_>, + worker: v8::Local<'_, v8::Object>, + context: &NestedWorkerContext, + script_url: &Url, + message: String, +) { + let _ = context + .wake_tx + .send(crate::worker::WorkerMessage::NestedWorkerEvent { + worker_id: context.worker_id, + message: Box::new(crate::worker::WorkerToParentMessage::Error { + message, + filename: script_url.to_string(), + lineno: 0, + colno: 0, + event_kind: crate::worker::WorkerParentErrorEventKind::Event, + phase: crate::worker::WorkerErrorPhase::Bootstrap, + source: crate::worker::WorkerErrorSource::Runtime, + }), + }); + set_private_value( + scope, + worker, + WORKER_ID_SLOT, + v8::Number::new(scope, context.worker_id.as_u64() as f64).into(), + ); +} + pub(in crate::context_bootstrap) fn worker_constructor_base_url( host: &crate::native_bridge::JsContextHost, ) -> Url { diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index fd96c5724b..9e12b3903f 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -766,6 +766,7 @@ pub(crate) use runtime_script_continuation::RuntimeScriptContinuationBodyEffect; #[cfg(test)] pub(crate) use runtime_script_continuation::RuntimeScriptOwnerAdvance; mod security_policy; +pub(crate) use security_policy::string_code_generation_check_callback; mod service_worker_client_message_body; #[cfg(test)] mod service_worker_client_message_test_support; diff --git a/moli-renderer-v8/src/script_vm/security_policy.rs b/moli-renderer-v8/src/script_vm/security_policy.rs index b47f8ebd51..1319472907 100644 --- a/moli-renderer-v8/src/script_vm/security_policy.rs +++ b/moli-renderer-v8/src/script_vm/security_policy.rs @@ -141,21 +141,35 @@ pub(super) unsafe extern "C" fn wasm_code_generation_check_callback( host.allows_wasm_code_generation_by_csp(scope) } -pub(super) fn string_code_generation_check_callback<'s>( +pub(crate) fn string_code_generation_check_callback<'s>( scope: &mut v8::PinScope<'s, '_>, source: v8::Local<'s, v8::Value>, is_code_like: bool, ) -> v8::ModifyCodeGenerationFromStringsResult<'s> { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - return code_generation_result(true, None); - }; - if crate::context_bootstrap::consume_internal_javascript_url_eval(scope) { - return code_generation_result(true, None); - } - let trusted_types_requirements = - unsafe { &*host_ptr }.trusted_types_for_script_requirements(scope); + let worker_trusted_types_requirements = + crate::worker::worker_trusted_types_for_script_requirements(scope); + let (trusted_types_requirements, allow_trusted_types_eval, host_ptr) = + if let Some(requirements) = worker_trusted_types_requirements { + ( + requirements, + crate::worker::worker_allows_trusted_types_eval(scope).unwrap_or(false), + None, + ) + } else { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return code_generation_result(true, None); + }; + if crate::context_bootstrap::consume_internal_javascript_url_eval(scope) { + return code_generation_result(true, None); + } + ( + unsafe { &*host_ptr }.trusted_types_for_script_requirements(scope), + unsafe { &*host_ptr }.allows_trusted_types_eval(scope), + Some(host_ptr), + ) + }; let action = if trusted_types_requirements.requires_conversion() { - if unsafe { &*host_ptr }.allows_trusted_types_eval(scope) { + if allow_trusted_types_eval { // The keyword relaxes Trusted Types conversion, but it does not // override another CSP policy. The per-policy CSP gate still runs. if source.is_string() { @@ -244,11 +258,20 @@ pub(super) fn string_code_generation_check_callback<'s>( source, modified_source: replacement, } => { - if !unsafe { &mut *host_ptr }.allows_eval_code_generation_by_csp( - scope, - allow_trusted_types_eval, - source.as_deref(), - ) { + let allowed = match host_ptr { + Some(host_ptr) => unsafe { &mut *host_ptr }.allows_eval_code_generation_by_csp( + scope, + allow_trusted_types_eval, + source.as_deref(), + ), + None => crate::worker::worker_allows_eval_code_generation_by_csp( + scope, + allow_trusted_types_eval, + source.as_deref(), + ) + .unwrap_or(false), + }; + if !allowed { return code_generation_result(false, None); } let replacement = match replacement { diff --git a/moli-renderer-v8/src/shared_worker_runtime/loading.rs b/moli-renderer-v8/src/shared_worker_runtime/loading.rs index 89cb132e57..d02e5cbfec 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/loading.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/loading.rs @@ -288,7 +288,7 @@ impl SharedWorkerScriptRequestPolicy { &self.document_content_security_policies, document_url, script_url, - ContentSecurityPolicyResourceKind::SharedWorkerScript, + ContentSecurityPolicyResourceKind::WorkerConstructor, || { format!( "Failed to load shared worker script `{script_url}`: blocked by Content Security Policy." 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 2c8fa8883c..c274595b96 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 @@ -5,9 +5,10 @@ use url::Url; use crate::RendererSyntheticResponseBody; use crate::content_security_policy::{ - ContentSecurityPolicyDisposition, ContentSecurityPolicyRedirectStatus, - ContentSecurityPolicyResourceKind, ContentSecurityPolicyUrlViolation, - ContentSecurityPolicyViolationEventFields, content_security_policy_report_requests, + ContentSecurityPolicyDisposition, ContentSecurityPolicyNonUrlKind, + ContentSecurityPolicyRedirectStatus, ContentSecurityPolicyResourceKind, + ContentSecurityPolicyUrlViolation, ContentSecurityPolicyViolationEventFields, + content_security_policy_non_url_violation_with_source, content_security_policy_report_requests, 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, @@ -835,6 +836,34 @@ pub(super) fn worker_content_security_policy_violation( ) } +pub(super) fn worker_eval_content_security_policy_violation( + state: &WorkerGlobalState, + protected_url: &Url, + allow_trusted_types_eval: bool, + 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| { + content_security_policy_non_url_violation_with_source( + policy, + protected_url, + kind, + source, + disposition, + &state.content_security_reporting_endpoints, + ) + }) +} + pub(super) fn worker_content_security_policy_violation_with_redirect_status( state: &WorkerGlobalState, protected_url: &Url, diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 263830d520..56c8f9a96d 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -52,7 +52,7 @@ use url::Url; use super::{ decode_data_url_script_source, handle::{ - WorkerConsoleMessage, WorkerFetchHandlerType, WorkerPendingFetchContinue, + WorkerConsoleMessage, WorkerFetchHandlerType, WorkerMessage, WorkerPendingFetchContinue, WorkerPendingSubresourceFetch, WorkerPendingXhrContinue, WorkerToParentMessage, WorkerWebSocketFrameEvent, WorkerWebSocketLifecycleEvent, }, @@ -159,6 +159,16 @@ pub(super) fn dispatch_worker_csp_violation_event<'s>( ); } +pub(super) fn dispatch_worker_csp_violation_event_for_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + state: &Rc>, + violation: &crate::content_security_policy::ContentSecurityPolicyUrlViolation, +) { + content_security_policy::dispatch_worker_content_security_policy_violation_event_for_state( + scope, state, violation, + ); +} + pub(super) const WORKER_GLOBAL_LISTENERS_SLOT: &str = "__moliWorkerGlobalListeners"; pub(crate) const WORKER_STATE_SLOT: &str = "__workerState"; const WORKER_GLOBAL_ONMESSAGE_SLOT: &str = "__moliWorkerGlobalOnMessage"; @@ -2869,6 +2879,50 @@ pub(crate) fn reserve_nested_worker_context( }) } +pub(crate) fn check_and_queue_nested_worker_constructor_csp( + scope: &mut v8::PinScope<'_, '_>, + request_url: &Url, +) -> Result<(), String> { + let state = get_worker_state(scope) + .expect("nested Worker construction requires an installed worker global state"); + let (wake_tx, report_only_violation, enforce_violation) = { + let state = state.borrow(); + let protected_url = state + .current_script_url + .as_ref() + .expect("nested Worker construction requires a current worker script URL"); + ( + state.worker_wake_tx.clone(), + worker_content_security_policy_report_only_violation( + &state, + protected_url, + request_url, + crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerConstructor, + ), + worker_content_security_policy_violation( + &state, + protected_url, + request_url, + crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerConstructor, + ), + ) + }; + + if let Some(violation) = report_only_violation { + let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation( + Box::new(violation), + )); + } + let Some(violation) = enforce_violation else { + return Ok(()); + }; + let message = worker_content_security_policy_error_message(&violation, "Worker"); + let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation( + Box::new(violation), + )); + Err(message) +} + pub(crate) fn worker_service_worker_control_state( scope: &mut v8::PinScope<'_, '_>, ) -> Option { @@ -3007,11 +3061,12 @@ pub(super) fn install_worker_global_scope<'s>( secure_context, )?; crate::context_bootstrap::install_trusted_types_runtime_state(scope, global)?; - let require_trusted_types_for_script = - crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( - &state.borrow().content_security_policies, - ); - if require_trusted_types_for_script { + let has_string_code_generation_policy = { + let state = state.borrow(); + !state.content_security_policies.is_empty() + || !state.content_security_report_only_policies.is_empty() + }; + if has_string_code_generation_policy { scope .get_current_context() .set_allow_generation_from_strings(false); @@ -4449,12 +4504,68 @@ pub(crate) fn worker_allows_trusted_types_eval(scope: &mut v8::PinScope<'_, '_>) ) } +pub(crate) fn worker_allows_eval_code_generation_by_csp( + scope: &mut v8::PinScope<'_, '_>, + allow_trusted_types_eval: bool, + source: Option<&str>, +) -> Option { + let state = get_worker_state(scope)?; + let (wake_tx, report_only_violation, enforce_violation) = { + let state = state.borrow(); + let Some(protected_url) = state.current_script_url.as_ref() else { + return Some(true); + }; + ( + state.worker_wake_tx.clone(), + worker_eval_content_security_policy_violation( + &state, + protected_url, + allow_trusted_types_eval, + source, + crate::content_security_policy::ContentSecurityPolicyDisposition::Report, + ), + worker_eval_content_security_policy_violation( + &state, + protected_url, + allow_trusted_types_eval, + source, + crate::content_security_policy::ContentSecurityPolicyDisposition::Enforce, + ), + ) + }; + if let Some(violation) = report_only_violation { + let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation( + Box::new(violation), + )); + } + let allowed = enforce_violation.is_none(); + if let Some(violation) = enforce_violation { + let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation( + Box::new(violation), + )); + } + Some(allowed) +} + pub(crate) fn worker_requires_trusted_types_for_script( scope: &mut v8::PinScope<'_, '_>, ) -> Option { + Some(worker_trusted_types_for_script_requirements(scope)?.is_enforced()) +} + +pub(crate) fn worker_trusted_types_for_script_requirements( + scope: &mut v8::PinScope<'_, '_>, +) -> Option { + let state = get_worker_state(scope)?; + let state = state.borrow(); Some( - crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( - &get_worker_state(scope)?.borrow().content_security_policies, + crate::content_security_policy::TrustedTypesForScriptRequirements::new( + crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( + &state.content_security_policies, + ), + crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( + &state.content_security_report_only_policies, + ), ), ) } diff --git a/moli-renderer-v8/src/worker/handle.rs b/moli-renderer-v8/src/worker/handle.rs index a444a2e6d3..f8c43212b2 100644 --- a/moli-renderer-v8/src/worker/handle.rs +++ b/moli-renderer-v8/src/worker/handle.rs @@ -139,6 +139,10 @@ pub(crate) enum WorkerMessage { ServiceWorkerGetNotificationsResult(ServiceWorkerGetNotificationsResult), /// Run the worker's queued unhandled promise rejection notification task. DispatchPendingPromiseRejections, + /// Dispatch a CSP violation queued while the current worker was still evaluating script. + DispatchContentSecurityPolicyViolation( + Box, + ), /// A worker spawned from this worker has queued a parent-facing event. NestedWorkerEvent { worker_id: DedicatedWorkerId, diff --git a/moli-renderer-v8/src/worker/mod.rs b/moli-renderer-v8/src/worker/mod.rs index 7f0731b898..6bae0d4591 100644 --- a/moli-renderer-v8/src/worker/mod.rs +++ b/moli-renderer-v8/src/worker/mod.rs @@ -25,7 +25,8 @@ mod timer_callback; pub(crate) use data_url::decode_data_url_script_source; pub(crate) use global_scope::{ NestedWorkerContext, WORKER_STATE_SLOT, WorkerOpfsCompletion, WorkerWebCryptoCompletion, - cancel_worker_opfs_task, check_worker_websocket_csp, close_worker_websocket, + cancel_worker_opfs_task, check_and_queue_nested_worker_constructor_csp, + check_worker_websocket_csp, close_worker_websocket, dispatch_worker_trusted_types_sink_violation_event, ensure_worker_opfs_directory_iterator_registry, ensure_worker_opfs_handle_registry, forget_nested_worker_context, forget_worker_broadcast_channel_wrapper, @@ -35,7 +36,8 @@ pub(crate) use global_scope::{ register_worker_webcrypto_task, register_worker_websocket, reserve_nested_worker_context, send_worker_websocket_binary, send_worker_websocket_text, service_worker_runtime_identity, try_worker_xhr_abort_callback, try_worker_xhr_reschedule_timeout_after_timeout_change, - try_worker_xhr_send_callback, worker_allows_trusted_type_policy_name_by_csp, + try_worker_xhr_send_callback, worker_allows_eval_code_generation_by_csp, + 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, @@ -43,7 +45,8 @@ pub(crate) use global_scope::{ worker_notification_permission_state, worker_opfs_directory_iterator_registry, worker_opfs_handle_registry, worker_requires_trusted_types_for_script, worker_service_worker_control_state, worker_storage_key, worker_storage_partition_identity, - worker_termination_requested, worker_uses_shared_worker_agent_cluster, + worker_termination_requested, worker_trusted_types_for_script_requirements, + worker_uses_shared_worker_agent_cluster, }; pub(crate) use handle::WorkerMessage; pub(crate) use handle::{ diff --git a/moli-renderer-v8/src/worker/thread/isolate.rs b/moli-renderer-v8/src/worker/thread/isolate.rs index 64e0324669..1c1893ad0f 100644 --- a/moli-renderer-v8/src/worker/thread/isolate.rs +++ b/moli-renderer-v8/src/worker/thread/isolate.rs @@ -43,7 +43,7 @@ impl WorkerIsolateState { worker_dynamic_import_with_phase_callback, ); isolate.set_modify_code_generation_from_strings_callback( - crate::context_bootstrap::trusted_types_code_generation_check_callback, + crate::script_vm::string_code_generation_check_callback, ); let runtime_inspector = WorkerRuntimeInspector::new( &mut isolate, diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index c5b96d6966..ee9014f4cc 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -73,9 +73,10 @@ use super::global_scope::{ continue_pending_worker_fetch, continue_pending_worker_fetch_response, continue_pending_worker_xhr, continue_pending_worker_xhr_response, dispatch_nested_worker_event, dispatch_worker_csp_violation_event, - dispatch_worker_websocket_event, drain_service_worker_client_focus_result, - drain_service_worker_client_navigate_result, drain_service_worker_client_query_result, - drain_service_worker_clients_open_window_result, drain_service_worker_get_notifications_result, + dispatch_worker_csp_violation_event_for_state, dispatch_worker_websocket_event, + drain_service_worker_client_focus_result, drain_service_worker_client_navigate_result, + drain_service_worker_client_query_result, drain_service_worker_clients_open_window_result, + drain_service_worker_get_notifications_result, drain_service_worker_periodic_sync_get_tags_result, drain_service_worker_periodic_sync_registration_result, drain_service_worker_periodic_sync_unregistration_result, @@ -2660,6 +2661,23 @@ async fn worker_main( perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx); } + WorkerLoopWake::Message(Some( + WorkerMessage::DispatchContentSecurityPolicyViolation(violation), + )) => { + if pending_module_bootstrap.is_some() { + pending_bootstrap_messages.push_back( + WorkerMessage::DispatchContentSecurityPolicyViolation(violation), + ); + continue; + } + let scope = pin!(v8::HandleScope::new(worker_isolate.worker_isolate_mut())); + let scope = &mut scope.init(); + let ctx = v8::Local::new(scope, &context); + let scope = &mut v8::ContextScope::new(scope, ctx); + dispatch_worker_csp_violation_event_for_state(scope, &state, &violation); + perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); + drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx); + } WorkerLoopWake::Message(Some(WorkerMessage::NestedWorkerEvent { worker_id, message, diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_errors_and_teardown.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_errors_and_teardown.rs index 6987265257..242bb6e86a 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_errors_and_teardown.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_errors_and_teardown.rs @@ -1622,3 +1622,107 @@ async fn worker_drop_terminates() { drop(handle); // If we reach here without hanging, the test passes. } + +#[tokio::test] +async fn nested_worker_constructor_csp_block_is_async_and_reports_to_parent_global() { + ensure_v8(); + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new( + r#" + const result = { + constructed: false, + violation: null, + error: null, + ping: false + }; + function finish() { + if (result.violation && result.error) { + postMessage(result); + close(); + } + } + const child = new Worker("data:text/javascript,postMessage('ping')"); + child.addEventListener("message", () => { + result.ping = true; + postMessage(result); + close(); + }); + addEventListener("securitypolicyviolation", event => { + result.violation = { + type: event.type, + effectiveDirective: event.effectiveDirective, + violatedDirective: event.violatedDirective, + blockedURI: event.blockedURI, + documentURI: event.documentURI, + originalPolicy: event.originalPolicy, + disposition: event.disposition, + instance: event instanceof SecurityPolicyViolationEvent + }; + finish(); + }); + child.addEventListener("error", event => { + event.preventDefault(); + result.error = { + messageIncludesCsp: event.message.includes("Content Security Policy"), + filename: event.filename + }; + finish(); + }); + result.constructed = true; + "# + .into(), + "https://app.example/parent.js".into(), + ) + .with_content_security_policies(vec!["worker-src 'none'".to_owned()]), + ); + + assert_eq!( + recv_post_json(&mut handle).await, + r#"{"constructed":true,"violation":{"type":"securitypolicyviolation","effectiveDirective":"worker-src","violatedDirective":"worker-src","blockedURI":"data","documentURI":"https://app.example/parent.js","originalPolicy":"worker-src 'none'","disposition":"enforce","instance":true},"error":{"messageIncludesCsp":true,"filename":"data:text/javascript,postMessage('ping')"},"ping":false}"# + ); +} + +#[tokio::test] +async fn nested_worker_constructor_report_only_csp_is_async_and_does_not_block() { + ensure_v8(); + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new( + r#" + const result = { + constructed: false, + violation: null, + childMessage: null + }; + function finish() { + if (result.violation && result.childMessage) { + postMessage(result); + close(); + } + } + const child = new Worker("data:text/javascript,postMessage('ping')"); + addEventListener("securitypolicyviolation", event => { + result.violation = { + effectiveDirective: event.effectiveDirective, + blockedURI: event.blockedURI, + disposition: event.disposition, + instance: event instanceof SecurityPolicyViolationEvent + }; + finish(); + }); + child.addEventListener("message", event => { + result.childMessage = event.data; + finish(); + }); + result.constructed = true; + "# + .into(), + "https://app.example/parent.js".into(), + ) + .with_content_security_report_only_policies(vec!["worker-src 'none'".to_owned()]), + ); + + assert_eq!( + recv_post_json(&mut handle).await, + r#"{"constructed":true,"violation":{"effectiveDirective":"worker-src","blockedURI":"data","disposition":"report","instance":true},"childMessage":"ping"}"# + ); +} diff --git a/moli-renderer-v8/src/worker/thread/tests/modules.rs b/moli-renderer-v8/src/worker/thread/tests/modules.rs index de0f0c9a95..e4036e6f51 100644 --- a/moli-renderer-v8/src/worker/thread/tests/modules.rs +++ b/moli-renderer-v8/src/worker/thread/tests/modules.rs @@ -886,6 +886,101 @@ async fn worker_trusted_script_eval_is_unwrapped_with_trusted_types_eval_keyword assert_eq!(expect_post_json(msg), r#"{"trusted":7,"string":9}"#); } +#[tokio::test] +async fn worker_trusted_types_eval_keyword_requires_enforced_trusted_types() { + ensure_v8(); + for report_only_policies in [ + Vec::new(), + vec!["require-trusted-types-for 'script'".to_owned()], + ] { + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new( + r#" + let evalRan = false; + let errorName = null; + trustedTypes.createPolicy("default", { createScript: value => value }); + addEventListener("securitypolicyviolation", event => { + postMessage({ + evalRan, + errorName, + event: { + type: event.type, + effectiveDirective: event.effectiveDirective, + violatedDirective: event.violatedDirective, + blockedURI: event.blockedURI, + documentURI: event.documentURI, + originalPolicy: event.originalPolicy, + disposition: event.disposition, + instance: event instanceof SecurityPolicyViolationEvent, + }, + }); + close(); + }); + try { + eval("evalRan = true"); + errorName = "allowed"; + } catch (error) { + errorName = `${error.name}:${error instanceof EvalError}`; + } + postMessage({ phase: "evaluated", evalRan, errorName }); + "# + .to_owned(), + "https://app.test/worker/main.js".to_owned(), + ) + .with_content_security_policies(vec![ + "script-src 'self' 'trusted-types-eval'".to_owned(), + ]) + .with_content_security_report_only_policies(report_only_policies), + ); + + let evaluated = timeout(TIMEOUT, handle.recv()) + .await + .expect("timed out") + .expect("channel closed"); + assert_eq!( + expect_post_json(evaluated), + r#"{"phase":"evaluated","evalRan":false,"errorName":"EvalError:true"}"# + ); + let violation = timeout(TIMEOUT, handle.recv()) + .await + .expect("timed out waiting for violation") + .expect("channel closed"); + assert_eq!( + expect_post_json(violation), + r#"{"evalRan":false,"errorName":"EvalError:true","event":{"type":"securitypolicyviolation","effectiveDirective":"script-src","violatedDirective":"script-src","blockedURI":"eval","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src 'self' 'trusted-types-eval'","disposition":"enforce","instance":true}}"# + ); + } +} + +#[tokio::test] +async fn worker_trusted_types_eval_keyword_allows_eval_when_trusted_types_are_enforced() { + ensure_v8(); + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new( + r#" + let violations = 0; + addEventListener("securitypolicyviolation", () => violations++); + const value = eval("40 + 2"); + setTimeout(() => { + postMessage({ value, violations }); + close(); + }); + "# + .to_owned(), + "https://app.test/worker/main.js".to_owned(), + ) + .with_content_security_policies(vec![ + "script-src 'self' 'trusted-types-eval'; require-trusted-types-for 'script'".to_owned(), + ]), + ); + + let msg = timeout(TIMEOUT, handle.recv()) + .await + .expect("timed out") + .expect("channel closed"); + assert_eq!(expect_post_json(msg), r#"{"value":42,"violations":0}"#); +} + #[tokio::test] async fn worker_trusted_script_code_like_brand_drives_function_constructor() { ensure_v8();