mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 00:01:06 +00:00
fix(workers): enforce response CSP gates
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user