fix(workers): enforce response CSP gates

This commit is contained in:
ldm0
2026-10-02 03:27:49 +08:00
parent c35b1e0678
commit 3346ada44c
17 changed files with 484 additions and 108 deletions
@@ -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
@@ -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
+15 -15
View File
@@ -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"
));
}
@@ -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;
@@ -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;
@@ -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 {
+1
View File
@@ -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;
@@ -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 {
@@ -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."
@@ -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<ContentSecurityPolicyUrlViolation> {
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,
+119 -8
View File
@@ -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<RefCell<WorkerGlobalState>>,
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<crate::runtime::ServiceWorkerControlState> {
@@ -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<bool> {
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<bool> {
Some(worker_trusted_types_for_script_requirements(scope)?.is_enforced())
}
pub(crate) fn worker_trusted_types_for_script_requirements(
scope: &mut v8::PinScope<'_, '_>,
) -> Option<crate::content_security_policy::TrustedTypesForScriptRequirements> {
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,
),
),
)
}
+4
View File
@@ -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<crate::content_security_policy::ContentSecurityPolicyUrlViolation>,
),
/// A worker spawned from this worker has queued a parent-facing event.
NestedWorkerEvent {
worker_id: DedicatedWorkerId,
+6 -3
View File
@@ -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::{
@@ -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,
+21 -3
View File
@@ -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,
@@ -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"}"#
);
}
@@ -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();