fix(workers): preserve imported script origins and no-cors boundaries

Keep worker settings URLs stable while imported classic scripts retain their own dynamic-import base, request URL and muted-error metadata. Allow classic no-cors imports without relaxing module CORS or response security policies.

Preserve CSP source locations and pre-redirect URLs through worker callbacks and report serialization. Add regression coverage for redirects, CSP/CORP/COEP, opaque errors, rejection reporting and nested imports.

Validation: cargo fmt --all; full workspace clippy with warnings denied; nextest 17699 passed, 13 skipped. Before/after CLI and CDP comparisons cover 707 cases without losing an originally passing case. Promote only the worker importScripts base-URL WPT. Both smoke baselines are 42/43, with Puppeteer blocked by the same missing local puppeteer-core dependency.
This commit is contained in:
ldm0
2026-09-08 20:07:19 +08:00
parent d6311fbea1
commit bf8505975e
21 changed files with 964 additions and 192 deletions
@@ -2961,7 +2961,6 @@ html/semantics/scripting-1/the-script-element/json-module/json-module-service-wo
html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-window-onerror-module.html
html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-window-onerror.html
html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-workerglobalscope-onerror.html
html/semantics/scripting-1/the-script-element/module/dynamic-import/alpha/base-url-worker-importScripts.html
html/semantics/scripting-1/the-script-element/module/dynamic-import/code-cache-base-url.html
html/semantics/scripting-1/the-script-element/module/inline-async-execorder.html
html/semantics/scripting-1/the-script-element/moving-between-documents/ordering/delay-load-event-1.html
@@ -6393,6 +6393,7 @@ html/semantics/scripting-1/the-script-element/module/currentScript-null.html
html/semantics/scripting-1/the-script-element/module/custom-element-exception.html
html/semantics/scripting-1/the-script-element/module/duplicated-imports-1.html
html/semantics/scripting-1/the-script-element/module/duplicated-imports-2.html
html/semantics/scripting-1/the-script-element/module/dynamic-import/alpha/base-url-worker-importScripts.html
html/semantics/scripting-1/the-script-element/module/dynamic-import/code-cache-nonce.html
html/semantics/scripting-1/the-script-element/module/dynamic-import/delay-load-event.html
html/semantics/scripting-1/the-script-element/module/dynamic-import/dynamic-imports-script-error.html
@@ -219,8 +219,8 @@ impl<'a> ContentSecurityPolicyViolationEventFields<'a> {
disposition: violation.disposition,
source_file: violation.source_file.as_str(),
sample: violation.sample.as_str(),
line_number: 0,
column_number: 0,
line_number: violation.line_number,
column_number: violation.column_number,
status_code: 0,
}
}
@@ -477,7 +477,7 @@ pub(crate) fn content_security_policy_report_to_endpoints(
pub(crate) fn content_security_policy_violation_report_body(
fields: &ContentSecurityPolicyViolationEventFields<'_>,
) -> String {
json!({
let mut report = json!({
"csp-report": {
"document-uri": fields.document_uri,
"referrer": fields.referrer,
@@ -490,8 +490,14 @@ pub(crate) fn content_security_policy_violation_report_body(
"status-code": fields.status_code,
"script-sample": fields.sample,
}
})
.to_string()
});
if fields.line_number != 0 {
report["csp-report"]["line-number"] = json!(fields.line_number);
}
if fields.column_number != 0 {
report["csp-report"]["column-number"] = json!(fields.column_number);
}
report.to_string()
}
pub(crate) fn content_security_policy_reporting_api_report_body(
@@ -929,6 +935,34 @@ pub(crate) fn content_security_policy_source_file_for_report(source_file: &str)
source_url.to_string()
}
pub(crate) fn current_script_violation_location(
scope: &mut v8::PinScope<'_, '_>,
) -> Option<(String, i32, i32)> {
let stack = v8::StackTrace::current_stack_trace(scope, 1)?;
let frame = stack.get_frame(scope, 0)?;
// Imported worker scripts retain the originally requested URL separately
// from their final resource name and their (possibly muted) import base.
// Reporting the final URL could disclose a cross-origin redirect target.
let source_file = scope
.get_current_host_defined_options()
.and_then(|options| {
crate::util::script_request_url_from_host_defined_options(scope, options)
})
.map(|url| url.to_string())
.or_else(|| {
frame
.get_script_name_or_source_url(scope)
.map(|source| source.to_rust_string_lossy(scope))
})
.map(|source| content_security_policy_source_file_for_report(&source))
.unwrap_or_default();
let line_number = i32::try_from(frame.get_line_number())
.unwrap_or_default()
.max(0);
let column_number = i32::try_from(frame.get_column()).unwrap_or_default().max(0);
Some((source_file, line_number, column_number))
}
pub(crate) fn content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints(
policies: &[String],
protected_url: &Url,
@@ -2948,6 +2982,35 @@ mod tests {
);
}
#[test]
fn violation_report_formats_preserve_captured_script_locations() {
let mut violation = content_security_policy_url_violation_with_redirect_status(
&["connect-src 'none'".to_owned()],
&protected_url(),
&request_url("https://api.test/data.json"),
ContentSecurityPolicyResourceKind::WorkerConnect,
ContentSecurityPolicyRedirectStatus::NoRedirect,
)
.unwrap();
violation.source_file = "https://app.test/imported/script.js".into();
violation.line_number = 12;
violation.column_number = 34;
let fields = ContentSecurityPolicyViolationEventFields::from_url_violation(&violation);
assert_eq!(fields.source_file, violation.source_file);
assert_eq!((fields.line_number, fields.column_number), (12, 34));
let legacy: serde_json::Value =
serde_json::from_str(&content_security_policy_violation_report_body(&fields)).unwrap();
assert_eq!(legacy["csp-report"]["source-file"], violation.source_file);
assert_eq!(legacy["csp-report"]["line-number"], 12);
assert_eq!(legacy["csp-report"]["column-number"], 34);
let reporting: serde_json::Value =
serde_json::from_str(&content_security_policy_reporting_api_report_body(&fields))
.unwrap();
assert_eq!(reporting[0]["body"]["sourceFile"], violation.source_file);
assert_eq!(reporting[0]["body"]["lineNumber"], 12);
assert_eq!(reporting[0]["body"]["columnNumber"], 34);
}
#[test]
fn violation_report_request_uses_csp_fetch_security_modes() {
let violation = content_security_policy_url_violation_with_redirect_status(
@@ -46,6 +46,7 @@ pub(super) fn report_custom_element_construction_failure<'s>(
};
if let Some(constructor) = constructor {
let report = V8ExceptionReport {
muted_errors: false,
summary: message,
source: None,
line: None,
@@ -13,6 +13,8 @@ const VALUE_DEBUG_SUMMARY_MAX_BYTES: usize = 160 * 4;
const VALUE_DEBUG_OBJECT_SUMMARY_MAX_BYTES: usize = 240 * 4;
pub(super) struct V8ExceptionReport {
/// Script-origin taint supplied by V8, retained until web-facing reporting.
pub(super) muted_errors: bool,
pub(super) summary: String,
pub(super) source: Option<String>,
pub(super) line: Option<usize>,
@@ -155,6 +157,7 @@ pub(super) fn build_event_handler_exception_report<'s>(
.or_else(|| exception.and_then(|exception| exception_stack_property(scope, exception)));
let mut report = V8ExceptionReport {
muted_errors: message.is_some_and(|message| message.is_opaque()),
summary,
source,
line,
@@ -727,6 +730,7 @@ fn invoke_callback_with_report_inner<'s>(
if scope.is_execution_terminating() {
let _ = scope.rethrow();
captured_report = Some(V8ExceptionReport {
muted_errors: false,
summary: format!("{callback_kind} `{callback_name}` was terminated"),
source: None,
line: None,
@@ -757,6 +761,7 @@ fn invoke_callback_with_report_inner<'s>(
let _ = callback_kind;
returned_value.ok_or_else(|| {
Box::new(captured_report.unwrap_or_else(|| V8ExceptionReport {
muted_errors: false,
summary: format!("{callback_kind} `{callback_name}` threw"),
source: None,
line: None,
+1
View File
@@ -1269,6 +1269,7 @@ fn run_window_timer_source(
let mut scope = try_catch.init();
let Some(source_value) = v8_string(&scope, &source.source) else {
return Err(Box::new(V8ExceptionReport {
muted_errors: false,
summary: "failed to allocate timer source string".to_owned(),
source: Some(source.provenance.source_url().to_string()),
line: None,
@@ -4,6 +4,7 @@ use crate::{
ContentSecurityPolicyNonUrlKind, ContentSecurityPolicyRedirectStatus,
ContentSecurityPolicyReportingEndpoints, ContentSecurityPolicyScriptElementRequest,
ContentSecurityPolicyViolationEventFields, TrustedTypesForScriptRequirements,
current_script_violation_location,
},
context_bootstrap::CHILD_BROWSING_CONTEXT_HANDLE_SLOT,
document_runtime::{
@@ -1304,22 +1305,3 @@ impl JsContextHost {
Ok(())
}
}
fn current_script_violation_location(
scope: &mut v8::PinScope<'_, '_>,
) -> Option<(String, i32, i32)> {
let stack = v8::StackTrace::current_stack_trace(scope, 1)?;
let frame = stack.get_frame(scope, 0)?;
let source_file = frame
.get_script_name_or_source_url(scope)
.map(|source| source.to_rust_string_lossy(scope))
.map(|source| {
crate::content_security_policy::content_security_policy_source_file_for_report(&source)
})
.unwrap_or_default();
let line_number = i32::try_from(frame.get_line_number())
.unwrap_or_default()
.max(0);
let column_number = i32::try_from(frame.get_column()).unwrap_or_default().max(0);
Some((source_file, line_number, column_number))
}
@@ -5008,6 +5008,8 @@ fn create_module_script_origin<'s>(
base_url,
fetch_metadata.nonce(),
fetch_metadata.parser_inserted,
false,
None,
)
});
v8::ScriptOrigin::new(
+124 -7
View File
@@ -282,6 +282,33 @@ fn script_host_defined_options_as_fixed_array<'s>(
v8::Local::<v8::FixedArray>::try_from(host_defined_options).ok()
}
pub(crate) fn script_muted_errors_from_host_defined_options(
scope: &mut v8::PinScope<'_, '_>,
host_defined_options: v8::Local<'_, v8::Data>,
) -> Option<bool> {
// Validate the shared marker before reading the optional provenance field.
script_base_url_from_host_defined_options(scope, host_defined_options)?;
let options = script_host_defined_options_as_fixed_array(host_defined_options)?;
if options.length() < 5 {
return None;
}
let value = v8::Local::<v8::Value>::try_from(options.get(scope, 4)?).ok()?;
value.is_boolean().then_some(value.is_true())
}
pub(crate) fn script_request_url_from_host_defined_options(
scope: &mut v8::PinScope<'_, '_>,
host_defined_options: v8::Local<'_, v8::Data>,
) -> Option<Url> {
script_base_url_from_host_defined_options(scope, host_defined_options)?;
let options = script_host_defined_options_as_fixed_array(host_defined_options)?;
if options.length() < 6 {
return None;
}
let value = v8::Local::<v8::String>::try_from(options.get(scope, 5)?).ok()?;
Url::parse(&value.to_rust_string_lossy(scope)).ok()
}
pub(crate) fn script_base_url_from_continuation_data(
scope: &mut v8::PinScope<'_, '_>,
) -> Option<Url> {
@@ -302,7 +329,7 @@ pub(crate) fn script_host_defined_options_with_base_url_and_nonce<'s>(
base_url: &Url,
nonce: Option<&str>,
) -> Option<v8::Local<'s, v8::Data>> {
script_host_defined_options_with_fetch_metadata(scope, base_url, nonce, false)
script_host_defined_options_with_fetch_metadata(scope, base_url, nonce, false, false, None)
}
pub(crate) fn script_host_defined_options_with_fetch_metadata<'s>(
@@ -310,6 +337,8 @@ pub(crate) fn script_host_defined_options_with_fetch_metadata<'s>(
base_url: &Url,
nonce: Option<&str>,
parser_inserted: bool,
muted_errors: bool,
request_url: Option<&Url>,
) -> Option<v8::Local<'s, v8::Data>> {
let marker = v8_string(scope, SCRIPT_BASE_URL_HOST_DEFINED_OPTIONS_MARKER)?;
let value = v8_string(scope, base_url.as_str())?;
@@ -322,11 +351,14 @@ pub(crate) fn script_host_defined_options_with_fetch_metadata<'s>(
"not-parser-inserted"
},
)?;
let options = v8::PrimitiveArray::new(scope, 4);
let request_url = v8_string(scope, request_url.map(Url::as_str).unwrap_or_default())?;
let options = v8::PrimitiveArray::new(scope, 6);
options.set(scope, 0, marker.into());
options.set(scope, 1, value.into());
options.set(scope, 2, nonce.into());
options.set(scope, 3, parser_metadata.into());
options.set(scope, 4, v8::Boolean::new(scope, muted_errors).into());
options.set(scope, 5, request_url.into());
Some(options.into())
}
@@ -634,8 +666,10 @@ mod tests {
SCRIPT_BASE_URL_HOST_DEFINED_OPTIONS_MARKER, object_chain_contains,
script_base_url_from_host_defined_options,
script_host_defined_options_with_base_url_and_nonce,
script_host_defined_options_with_fetch_metadata, script_nonce_from_host_defined_options,
script_parser_inserted_from_host_defined_options, utf16_replace_units_range_lossy,
script_host_defined_options_with_fetch_metadata,
script_muted_errors_from_host_defined_options, script_nonce_from_host_defined_options,
script_parser_inserted_from_host_defined_options,
script_request_url_from_host_defined_options, utf16_replace_units_range_lossy,
utf16_slice_lossy, utf16_split_units_lossy, utf16_units, walk_object_chain,
};
use crate::ensure_v8_for_test as ensure_v8;
@@ -714,10 +748,93 @@ mod tests {
script_parser_inserted_from_host_defined_options(scope, options),
Some(false)
);
assert_eq!(
script_muted_errors_from_host_defined_options(scope, options),
Some(false)
);
let parser_options =
script_host_defined_options_with_fetch_metadata(scope, &base_url, Some("abc123"), true)
.expect("parser-inserted host-defined options should allocate");
assert_eq!(
script_request_url_from_host_defined_options(scope, options),
None
);
let request_url = Url::parse("https://request.test/redirect.js").unwrap();
let muted = script_host_defined_options_with_fetch_metadata(
scope,
&base_url,
None,
false,
true,
Some(&request_url),
)
.unwrap();
assert_eq!(
script_muted_errors_from_host_defined_options(scope, muted),
Some(true)
);
assert_eq!(
script_request_url_from_host_defined_options(scope, muted),
Some(request_url)
);
for fields in [
vec![
SCRIPT_BASE_URL_HOST_DEFINED_OPTIONS_MARKER,
base_url.as_str(),
],
vec![
SCRIPT_BASE_URL_HOST_DEFINED_OPTIONS_MARKER,
base_url.as_str(),
"",
"not-parser-inserted",
"true",
],
vec![
"not-moli",
base_url.as_str(),
"",
"not-parser-inserted",
"true",
],
] {
let untrusted = primitive_host_defined_options(scope, &fields);
assert_eq!(
script_muted_errors_from_host_defined_options(scope, untrusted),
None
);
assert_eq!(
script_request_url_from_host_defined_options(scope, untrusted),
None
);
}
for (marker, request) in [
(SCRIPT_BASE_URL_HOST_DEFINED_OPTIONS_MARKER, "not a URL"),
("not-moli", "https://private.test/source.js"),
] {
let untrusted = primitive_host_defined_options(
scope,
&[
marker,
base_url.as_str(),
"",
"not-parser-inserted",
"",
request,
],
);
assert_eq!(
script_request_url_from_host_defined_options(scope, untrusted),
None
);
}
let parser_options = script_host_defined_options_with_fetch_metadata(
scope,
&base_url,
Some("abc123"),
true,
false,
None,
)
.expect("parser-inserted host-defined options should allocate");
assert_eq!(
script_parser_inserted_from_host_defined_options(scope, parser_options),
Some(true)
@@ -13,7 +13,8 @@ use crate::content_security_policy::{
content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints,
content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints,
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints,
create_security_policy_violation_event, send_content_security_policy_reports,
create_security_policy_violation_event, current_script_violation_location,
send_content_security_policy_reports,
};
use crate::context_bootstrap::dispatch_simple_event_target_event;
use crate::network::loads::{ResourceLoadDisposition, ResourceLoadKind, ResourceLoadLease};
@@ -104,7 +105,14 @@ pub(super) fn dispatch_worker_trusted_types_sink_violation_event_for_state<'s>(
&state_ref.content_security_reporting_endpoints,
)
};
if let Some(violation) = violation {
if let Some(mut violation) = violation {
if let Some((source_file, line_number, column_number)) =
current_script_violation_location(scope)
{
violation.source_file = source_file;
violation.line_number = line_number;
violation.column_number = column_number;
}
dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation);
}
}
@@ -115,7 +123,7 @@ pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>(
policy_name: &str,
is_duplicate: bool,
) -> bool {
let (report_only_violation, enforced_violation) = {
let (mut report_only_violation, mut enforced_violation) = {
let state_ref = state.borrow();
let Some(protected_url) = state_ref.current_script_url.as_ref() else {
return true;
@@ -141,6 +149,21 @@ pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>(
(report_only_violation, enforced_violation)
};
let allowed = enforced_violation.is_none();
if allowed && report_only_violation.is_none() {
return true;
}
if let Some((source_file, line_number, column_number)) =
current_script_violation_location(scope)
{
for violation in [&mut report_only_violation, &mut enforced_violation]
.into_iter()
.flatten()
{
violation.source_file = source_file.clone();
violation.line_number = line_number;
violation.column_number = column_number;
}
}
// Match window policy creation reporting: enforce response policies are
// modeled before report-only policies in the partitioned policy state.
if let Some(violation) = enforced_violation {
@@ -1,9 +1,14 @@
use super::*;
use crate::content_security_policy::{
ContentSecurityPolicyRedirectStatus, ContentSecurityPolicyResourceKind,
};
use moli_url::WebOrigin;
pub(super) struct WorkerImportScriptSource {
pub(super) final_url: Url,
pub(super) source: String,
pub(super) muted_errors: bool,
redirect_urls: Vec<Url>,
resource: Option<crate::worker::WorkerScriptResource>,
}
@@ -41,34 +46,13 @@ pub(super) fn materialize_worker_import_source(
state: &Rc<RefCell<WorkerGlobalState>>,
script_url: &Url,
) -> Result<WorkerImportScriptSource, WorkerImportScriptError> {
if let Some(violation) = {
let state = state.borrow();
state.current_script_url.as_ref().and_then(|protected_url| {
worker_content_security_policy_report_only_violation(
&state,
protected_url,
script_url,
crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerScript,
)
})
} {
dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation);
}
if let Some(violation) = {
let state = state.borrow();
state.current_script_url.as_ref().and_then(|protected_url| {
worker_content_security_policy_violation(
&state,
protected_url,
script_url,
crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerScript,
)
})
} {
dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation);
let message = worker_content_security_policy_error_message(&violation, "importScripts");
return Err(WorkerImportScriptError::network(message));
}
check_import_script_csp(
scope,
state,
script_url,
script_url,
ContentSecurityPolicyRedirectStatus::NoRedirect,
)?;
match script_url.scheme() {
"data" => {
let source =
@@ -85,6 +69,7 @@ pub(super) fn materialize_worker_import_source(
final_url: script_url.clone(),
source,
muted_errors: false,
redirect_urls: Vec::new(),
resource: None,
})
}
@@ -101,32 +86,43 @@ pub(super) fn materialize_worker_import_source(
final_url: script_url.clone(),
source: body,
muted_errors: false,
redirect_urls: Vec::new(),
resource: None,
})
}
"http" | "https" => {
let (loader, initiator_url, referrer_policy, network_partition_key) = {
let (loader, initiator_url, referrer_policy, network_partition_key, policy_context) = {
let state = state.borrow();
(
state.loader.clone(),
state.current_script_url.clone(),
state.referrer_policy.clone(),
state.network_partition_key.clone(),
state.policy_context,
)
};
fetch_worker_import_source_blocking(
let source = fetch_worker_import_source_blocking(
loader,
script_url.clone(),
initiator_url,
referrer_policy,
network_partition_key,
policy_context,
)
.inspect(|source| {
if let Some(resource) = source.resource.clone() {
report_service_worker_imported_script_loaded(state, resource);
}
})
.map_err(WorkerImportScriptError::network)
.map_err(WorkerImportScriptError::network)?;
for checked_url in &source.redirect_urls {
check_import_script_csp(
scope,
state,
script_url,
checked_url,
ContentSecurityPolicyRedirectStatus::FollowedRedirect,
)?;
}
if let Some(resource) = source.resource.clone() {
report_service_worker_imported_script_loaded(state, resource);
}
Ok(source)
}
scheme => Err(WorkerImportScriptError::network(format!(
"Failed to execute 'importScripts': URL scheme `{scheme}` is not allowed."
@@ -134,6 +130,39 @@ pub(super) fn materialize_worker_import_source(
}
}
fn check_import_script_csp(
scope: &mut v8::PinScope<'_, '_>,
state: &Rc<RefCell<WorkerGlobalState>>,
request_url: &Url,
checked_url: &Url,
redirect_status: ContentSecurityPolicyRedirectStatus,
) -> Result<(), WorkerImportScriptError> {
let (report, enforce) = {
let state = state.borrow();
let Some(protected_url) = state.current_script_url.as_ref() else {
return Ok(());
};
(
worker_content_security_policy_report_only_violation_for_checked_url_with_redirect_status(
&state, protected_url, checked_url, request_url, ContentSecurityPolicyResourceKind::WorkerScript, redirect_status,
),
worker_content_security_policy_violation_for_checked_url_with_redirect_status(
&state, protected_url, checked_url, request_url, ContentSecurityPolicyResourceKind::WorkerScript, redirect_status,
),
)
};
if let Some(violation) = report {
dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation);
}
if let Some(violation) = enforce {
dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation);
return Err(WorkerImportScriptError::network(
worker_content_security_policy_error_message(&violation, "importScripts"),
));
}
Ok(())
}
fn ensure_worker_import_script_mime_acceptable(
script_url: &Url,
mime_type: &str,
@@ -150,22 +179,23 @@ pub(super) fn fetch_worker_import_source_blocking(
initiator_url: Option<Url>,
referrer_policy: Option<String>,
network_partition_key: Option<String>,
policy_context: crate::types::SubresourcePolicyContext,
) -> Result<WorkerImportScriptSource, String> {
let request_url = script_url.clone();
let mut request = moli_fetch::Request::new("GET", script_url.as_str(), None, vec![])
.map_err(|error| error.to_string())?
.with_page_network_policy()
.with_request_mode(RequestMode::NoCors)
.with_credentials_mode(RequestCredentialsMode::SameOrigin)
.with_script_fetch_metadata(moli_fetch::ScriptFetchRequestMetadata {
document_referrer_policy: referrer_policy,
..moli_fetch::ScriptFetchRequestMetadata::default()
})
.with_network_partition_key(network_partition_key);
let request_initiator_url = initiator_url.clone();
if let Some(ref initiator_url) = request_initiator_url {
request = request.with_initiator_url(initiator_url);
}
if let Some(referrer_policy) = referrer_policy {
request = request.with_script_fetch_metadata(moli_fetch::ScriptFetchRequestMetadata {
document_referrer_policy: Some(referrer_policy),
..moli_fetch::ScriptFetchRequestMetadata::default()
});
}
let response_started_at = Instant::now();
let cancel_handle = FetchCancelHandle::new();
let load = loader
@@ -185,22 +215,47 @@ pub(super) fn fetch_worker_import_source_blocking(
.elapsed()
.as_millis()
.min(u64::MAX as u128) as u64;
moli_fetch::ensure_http_status_success(response.final_url.as_str(), response.status, false)
.map_err(|error| error.to_string())?;
crate::worker::ensure_worker_script_mime_acceptable(
&response.final_url,
&response.headers,
response.body_bytes(),
)?;
if request_initiator_url.as_ref().is_some_and(|initiator_url| {
matches!(initiator_url.scheme(), "http" | "https")
&& !moli_url::same_origin(initiator_url, &response.final_url)
}) {
return Err(format!(
"Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at '{}' failed to load.",
response.final_url
));
}
// Classic imported scripts use no-cors, unlike the worker's top-level
// same-origin fetch and module CORS fetches. Any cross-origin response in
// the URL chain taints the result, even if it redirects back to the worker.
let muted_errors = request_initiator_url.as_ref().is_some_and(|initiator_url| {
!moli_url::same_origin(initiator_url, &request_url)
|| !moli_url::same_origin(initiator_url, &response.final_url)
|| response.redirect_chain.iter().any(|redirect| {
!moli_url::same_origin(initiator_url, &redirect.from_url)
|| !moli_url::same_origin(initiator_url, &redirect.to_url)
})
});
let response_validation = (|| {
moli_fetch::ensure_http_status_success(response.final_url.as_str(), response.status, false)
.map_err(|error| error.to_string())?;
crate::worker::ensure_worker_script_mime_acceptable(
&response.final_url,
&response.headers,
response.body_bytes(),
)?;
if let Some(initiator_url) = &request_initiator_url {
validate_fetch_response_security_policy_for_origin(
initiator_url,
&WebOrigin::from_url(initiator_url),
&response.final_url,
&response.headers,
RequestMode::NoCors,
RequestCredentialsMode::SameOrigin,
policy_context,
)?;
}
Ok::<_, String>(())
})();
response_validation.map_err(|error| {
if muted_errors {
format!(
"Failed to execute 'importScripts': The script at '{script_url}' failed to load."
)
} else {
error
}
})?;
let (head, body, body_bytes) = response.into_parts();
let resource = crate::worker::WorkerScriptResource::from_response_parts(
request_url,
@@ -211,7 +266,12 @@ pub(super) fn fetch_worker_import_source_blocking(
Ok(WorkerImportScriptSource {
final_url: head.final_url,
source: body,
muted_errors: false,
muted_errors,
redirect_urls: head
.redirect_chain
.into_iter()
.map(|redirect| redirect.to_url)
.collect(),
resource: Some(resource),
})
}
@@ -240,74 +300,95 @@ fn report_service_worker_imported_script_loaded(
pub(super) fn evaluate_worker_script(
scope: &mut v8::PinScope<'_, '_>,
state: Rc<RefCell<WorkerGlobalState>>,
request_url: &Url,
script_url: &Url,
script_source: &str,
muted_errors: bool,
) -> Result<(), WorkerImportScriptError> {
let previous_url = {
let mut state = state.borrow_mut();
state.current_script_url.replace(script_url.clone())
let source = v8::String::new(scope, script_source).ok_or_else(|| {
WorkerImportScriptError::error(
scope,
format!("failed to allocate worker source for `{script_url}`"),
)
})?;
let name = v8::String::new(scope, script_url.as_str()).expect("worker script origin");
let sanitized_base = Url::parse("about:blank").expect("valid sanitized script base");
let base_url = if muted_errors {
&sanitized_base
} else {
script_url
};
let outcome = (|| {
let source = v8::String::new(scope, script_source).ok_or_else(|| {
WorkerImportScriptError::error(
scope,
format!("failed to allocate worker source for `{script_url}`"),
)
})?;
let origin = create_script_origin(scope, script_url.as_str());
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
let mut scope = try_catch.init();
let Some(script) = v8::Script::compile(&scope, source, Some(&origin)) else {
if muted_errors {
return Err(WorkerImportScriptError::network(format!(
"Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at '{script_url}' failed to load."
)));
}
let error = scope
.exception()
.map(|value| {
let message = scope.message();
annotate_worker_exception_location(&mut scope, value, message);
WorkerImportScriptError::Exception(v8::Global::new(&scope, value))
})
.unwrap_or_else(|| {
WorkerImportScriptError::error(
&mut scope,
format!("failed to compile `{script_url}`"),
)
});
return Err(error);
};
let _ = script.run(&scope);
if scope.has_caught() {
if muted_errors {
return Err(WorkerImportScriptError::network(format!(
"Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at '{script_url}' failed to load."
)));
}
let error = scope
.exception()
.map(|value| {
let message = scope.message();
annotate_worker_exception_location(&mut scope, value, message);
WorkerImportScriptError::Exception(v8::Global::new(&scope, value))
})
.unwrap_or_else(|| {
WorkerImportScriptError::error(
&mut scope,
format!("failed to execute `{script_url}`"),
)
});
return Err(error);
let host_defined_options = crate::util::script_host_defined_options_with_fetch_metadata(
scope,
base_url,
None,
false,
muted_errors,
Some(request_url),
);
let origin = v8::ScriptOrigin::new(
scope,
name.into(),
0,
0,
false,
-1,
None,
muted_errors,
false,
false,
host_defined_options,
);
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
let mut scope = try_catch.init();
let Some(script) = v8::Script::compile(&scope, source, Some(&origin)) else {
if muted_errors {
return Err(WorkerImportScriptError::network(
"Failed to execute 'importScripts': A cross-origin script failed to execute."
.to_owned(),
));
}
scope.perform_microtask_checkpoint();
crate::context_bootstrap::run_end_of_microtask_checkpoint_tasks(&mut scope);
Ok(())
})();
state.borrow_mut().current_script_url = previous_url;
outcome
let error = scope
.exception()
.map(|value| {
let message = scope.message();
annotate_worker_exception_location(&mut scope, value, message);
WorkerImportScriptError::Exception(v8::Global::new(&scope, value))
})
.unwrap_or_else(|| {
WorkerImportScriptError::error(
&mut scope,
format!("failed to compile `{script_url}`"),
)
});
return Err(error);
};
let _ = script.run(&scope);
if scope.has_caught() {
if muted_errors {
return Err(WorkerImportScriptError::network(
"Failed to execute 'importScripts': A cross-origin script failed to execute."
.to_owned(),
));
}
let error = scope
.exception()
.map(|value| {
let message = scope.message();
annotate_worker_exception_location(&mut scope, value, message);
WorkerImportScriptError::Exception(v8::Global::new(&scope, value))
})
.unwrap_or_else(|| {
WorkerImportScriptError::error(
&mut scope,
format!("failed to execute `{script_url}`"),
)
});
return Err(error);
}
scope.perform_microtask_checkpoint();
crate::context_bootstrap::run_end_of_microtask_checkpoint_tasks(&mut scope);
Ok(())
}
// ─── console ────────────────────────────────────────────────────────────────
@@ -1497,7 +1497,8 @@ pub(crate) struct WorkerGlobalState {
pub(super) global_kind: super::thread::WorkerGlobalKind,
/// Whether this worker was constructed as a classic or module worker.
pub(super) script_kind: super::thread::WorkerScriptKind,
/// Base URL used to resolve relative worker script fetches.
/// Worker global's URL and settings base, unchanged by importScripts().
/// Imported scripts carry their separate import base in V8 ScriptOrigin.
pub(super) current_script_url: Option<Url>,
/// Referrer policy parsed from the top-level worker script response.
pub(super) referrer_policy: Option<String>,
@@ -6842,6 +6843,7 @@ fn worker_import_scripts_callback<'s>(
});
}
for mut script in prepared {
let request_url = script.final_url.clone();
if script.source.is_none() {
let import_source =
match materialize_worker_import_source(scope, &state, &script.final_url) {
@@ -6858,7 +6860,7 @@ fn worker_import_scripts_callback<'s>(
let source = script.source.as_deref().unwrap_or_default();
if let Err(error) = evaluate_worker_script(
scope,
state.clone(),
&request_url,
&script.final_url,
source,
script.muted_errors,
@@ -7024,23 +7026,6 @@ fn call_original_worker_console_method<'s>(
// ─── timers (minimal stubs) ─────────────────────────────────────────────────
fn create_script_origin<'s>(scope: &mut v8::PinScope<'s, '_>, url: &str) -> v8::ScriptOrigin<'s> {
let name = v8::String::new(scope, url).expect("worker script origin");
v8::ScriptOrigin::new(
scope,
name.into(),
0,
0,
false,
-1,
None,
false,
false,
false,
None,
)
}
fn set_prop(
scope: &mut v8::PinScope<'_, '_>,
obj: v8::Local<'_, v8::Object>,
@@ -2649,6 +2649,7 @@ fn worker_bootstrap_error(
v8::String::new(scope, summary).map(|message| v8::Exception::syntax_error(scope, message));
(
V8ExceptionReport {
muted_errors: false,
summary: summary.to_owned(),
source: Some(script_url.to_owned()),
line: Some(1),
@@ -2675,6 +2676,7 @@ fn worker_bootstrap_value_error(
.unwrap_or_else(|| "module worker evaluation failed".to_owned());
(
V8ExceptionReport {
muted_errors: false,
summary,
source: Some(script_url.to_owned()),
line: Some(1),
@@ -3060,13 +3062,14 @@ fn worker_import_attributes_key(
pub(super) fn worker_dynamic_import_callback<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
_host_defined_options: v8::Local<'s, v8::Data>,
host_defined_options: v8::Local<'s, v8::Data>,
resource_name: v8::Local<'s, v8::Value>,
specifier: v8::Local<'s, v8::String>,
import_attributes: v8::Local<'s, v8::FixedArray>,
) -> Option<v8::Local<'s, v8::Promise>> {
queue_worker_dynamic_import(
scope,
host_defined_options,
resource_name,
specifier,
import_attributes,
@@ -3076,6 +3079,7 @@ pub(super) fn worker_dynamic_import_callback<'s, 'i>(
fn queue_worker_dynamic_import<'s>(
scope: &mut v8::PinScope<'s, '_>,
host_defined_options: v8::Local<'s, v8::Data>,
resource_name: v8::Local<'s, v8::Value>,
specifier: v8::Local<'s, v8::String>,
import_attributes: v8::Local<'s, v8::FixedArray>,
@@ -3108,7 +3112,9 @@ fn queue_worker_dynamic_import<'s>(
);
return Some(promise);
}
let base_url = resource_url;
let base_url =
crate::util::script_base_url_from_host_defined_options(scope, host_defined_options)
.or(resource_url);
let Some(base_url) = base_url else {
reject_worker_dynamic_import_resolver(
scope,
@@ -3162,6 +3168,7 @@ pub(super) fn worker_dynamic_import_with_phase_callback<'s, 'i>(
}
queue_worker_dynamic_import(
scope,
host_defined_options,
resource_name,
specifier,
import_attributes,
+23 -1
View File
@@ -553,6 +553,15 @@ unsafe extern "C" fn worker_promise_reject_callback(message: v8::PromiseRejectMe
match message.get_event() {
v8::PromiseRejectEvent::PromiseRejectWithNoHandler => {
if scope
.get_current_host_defined_options()
.is_some_and(|options| {
crate::util::script_muted_errors_from_host_defined_options(scope, options)
== Some(true)
})
{
return;
}
let promise = message.get_promise();
let mut pending = pending_unhandled_rejections.borrow_mut();
if pending.iter().any(|rejection| {
@@ -4325,7 +4334,20 @@ pub(super) fn dispatch_worker_exception_with_phase_and_source<'s>(
parent_tx: &mpsc::UnboundedSender<WorkerToParentMessage>,
script_url: &str,
) -> bool {
apply_worker_exception_location_overrides(scope, &mut report, exception);
let exception = if report.muted_errors {
report.summary = "Script error.".to_owned();
report.source = Some(String::new());
report.line = Some(0);
report.column = Some(0);
report.source_line = None;
report.stack = None;
report.callback_context = None;
report.exception = None;
Some(v8::null(scope).into())
} else {
apply_worker_exception_location_overrides(scope, &mut report, exception);
exception
};
let handled =
dispatch_worker_error_event(scope, global, &report, exception, parent_tx, script_url);
if !handled {
@@ -2682,6 +2682,7 @@ async fn worker_main(
if let Some(error) = result.unhandled_error {
let global = ctx.global(scope);
let report = V8ExceptionReport {
muted_errors: false,
summary: error.message,
source: Some(error.filename),
line: Some(error.lineno as usize),
@@ -3493,6 +3494,7 @@ fn worker_bootstrap_error(
v8::String::new(scope, summary).map(|message| v8::Exception::syntax_error(scope, message));
(
V8ExceptionReport {
muted_errors: false,
summary: summary.to_owned(),
source: Some(script_url.to_owned()),
line: Some(1),
@@ -0,0 +1,480 @@
use super::*;
#[tokio::test]
async fn worker_importscripts_trusted_types_reports_keep_script_and_document_locations_separate() {
ensure_v8();
let source = "self.violatePolicy = () => {\n try { trustedTypes.createPolicy('forbidden'); } catch {}\n};\nself.violateSink = () => {\n try { setTimeout('blocked code'); } catch {}\n};\nviolatePolicy();\nviolateSink();";
let (base_url, server) = spawn_path_response_http_server(vec![(
"/imported/violations.js",
"HTTP/1.1 200 OK",
"text/javascript",
source.into(),
Duration::ZERO,
)])
.await;
let worker_url = format!("{base_url}/worker/main.js");
let options = WorkerSpawnOptions::new(
r#"
const violations = [];
addEventListener('securitypolicyviolation', event => violations.push([
event.effectiveDirective, event.documentURI, event.sourceFile,
event.lineNumber, event.columnNumber > 0
]));
const setup = trustedTypes.createPolicy('bootstrap', { createScriptURL: s => s });
importScripts(setup.createScriptURL('../imported/violations.js'));
setTimeout(() => {
violatePolicy(); violateSink();
postMessage(violations); close();
}, 0);
"#
.into(),
worker_url.clone(),
)
.with_content_security_policies(vec![
"require-trusted-types-for 'script'; trusted-types bootstrap".into(),
]);
let mut handle = spawn_test_worker_with_options(options);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
let actual: serde_json::Value = serde_json::from_str(&expect_post_json(message)).unwrap();
let script_url = format!("{base_url}/imported/violations.js");
assert_eq!(
actual,
serde_json::json!([
["trusted-types", worker_url, script_url, 2, true],
["require-trusted-types-for", worker_url, script_url, 5, true],
["trusted-types", worker_url, script_url, 2, true],
["require-trusted-types-for", worker_url, script_url, 5, true],
])
);
server.await.unwrap();
}
#[tokio::test]
async fn worker_importscripts_trusted_types_reports_do_not_expose_redirect_targets() {
ensure_v8();
let (foreign_url, foreign_server) = spawn_path_response_http_server(vec![(
"/private-user/violations.js?credential=hidden",
"HTTP/1.1 200 OK",
"text/javascript",
"setTimeout(() => {\n try { trustedTypes.createPolicy('forbidden'); } catch {}\n postMessage(violations); close();\n}, 0);".into(),
Duration::ZERO,
)])
.await;
let redirect = format!(
"HTTP/1.1 302 Found\r\nLocation: {foreign_url}/private-user/violations.js?credential=hidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
);
let (base_url, server) = spawn_raw_path_response_http_server(vec![(
"/worker/redirect.js",
redirect,
Duration::ZERO,
)])
.await;
let worker_url = format!("{base_url}/worker/main.js");
let options = WorkerSpawnOptions::new(
r#"
const violations = [];
addEventListener('securitypolicyviolation', event => violations.push([
event.disposition, event.documentURI, event.sourceFile,
event.lineNumber, event.columnNumber > 0
]));
importScripts('./redirect.js');
"#
.into(),
worker_url.clone(),
)
.with_content_security_policies(vec!["trusted-types 'none'".into()])
.with_content_security_report_only_policies(vec!["trusted-types 'none'".into()]);
let mut handle = spawn_test_worker_with_options(options);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
let actual: serde_json::Value = serde_json::from_str(&expect_post_json(message)).unwrap();
let request_url = format!("{base_url}/worker/redirect.js");
assert_eq!(
actual,
serde_json::json!([
["enforce", worker_url, request_url, 2, true],
["report", worker_url, request_url, 2, true],
])
);
server.await.unwrap();
foreign_server.await.unwrap();
}
#[tokio::test]
async fn worker_importscripts_cross_origin_respects_corp_and_coep() {
ensure_v8();
for (require_corp, corp, expected) in [
(false, "same-origin", r#"["NetworkError",false]"#),
(true, "", r#"["NetworkError",false]"#),
(true, "cross-origin", r#"["ok",true]"#),
] {
let body = "self.loaded = true;";
let corp_header = if corp.is_empty() {
String::new()
} else {
format!("Cross-Origin-Resource-Policy: {corp}\r\n")
};
let response = format!(
"HTTP/1.1 200 OK\r\n{corp_header}Content-Type: text/javascript\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
let (foreign_url, server) =
spawn_raw_path_response_http_server(vec![("/foreign.js", response, Duration::ZERO)])
.await;
let foreign = serde_json::to_string(&format!("{foreign_url}/foreign.js")).unwrap();
let policy_context = crate::types::SubresourcePolicyContext {
cross_origin_embedder_policy: if require_corp {
crate::cross_origin_isolation::CrossOriginEmbedderPolicy::RequireCorp
} else {
crate::cross_origin_isolation::CrossOriginEmbedderPolicy::None
},
..Default::default()
};
let options = WorkerSpawnOptions::new(
format!(
r#"
let outcome = 'ok';
try {{ importScripts({foreign}); }} catch (error) {{ outcome = error.name; }}
postMessage([outcome, self.loaded === true]); close();
"#
),
"http://127.0.0.1/worker/main.js".into(),
)
.with_policy_context(policy_context);
let mut handle = spawn_test_worker_with_options(options);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(
expect_post_json(message),
expected,
"require_corp={require_corp}, corp={corp}"
);
server.await.unwrap();
}
}
#[tokio::test]
async fn worker_importscripts_redirects_check_csp_and_ignore_redirected_paths() {
ensure_v8();
for (report_only, allow_foreign, expected) in [
(false, false, r#"["NetworkError",false,["enforce"]]"#),
(true, false, r#"["ok",true,["report"]]"#),
(false, true, r#"["ok",true,[]]"#),
] {
let (foreign_url, foreign_server) = spawn_path_response_http_server(vec![(
"/redirect-target/foreign.js",
"HTTP/1.1 200 OK",
"text/javascript",
"self.loaded = true;".into(),
Duration::ZERO,
)])
.await;
let response = format!(
"HTTP/1.1 302 Found\r\nLocation: {foreign_url}/redirect-target/foreign.js\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
);
let (worker_url, server) = spawn_raw_path_response_http_server(vec![(
"/worker/redirect.js",
response,
Duration::ZERO,
)])
.await;
let policy = if allow_foreign {
format!("script-src 'self' {foreign_url}/only-before-redirect/")
} else {
"script-src 'self'".to_owned()
};
let mut options = WorkerSpawnOptions::new(
r#"
const violations = [];
addEventListener('securitypolicyviolation', event => violations.push(event.disposition));
let outcome = 'ok';
try { importScripts('./redirect.js'); } catch (error) { outcome = error.name; }
postMessage([outcome, self.loaded === true, violations]); close();
"#.into(), format!("{worker_url}/worker/main.js"),
);
options = if report_only {
options.with_content_security_report_only_policies(vec![policy])
} else {
options.with_content_security_policies(vec![policy])
};
let mut handle = spawn_test_worker_with_options(options);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(
expect_post_json(message),
expected,
"report={report_only}, allow={allow_foreign}"
);
server.await.unwrap();
foreign_server.await.unwrap();
}
}
#[tokio::test]
async fn worker_importscripts_cross_origin_does_not_bypass_module_cors() {
ensure_v8();
let (foreign_url, server) = spawn_path_response_http_server(vec![
("/foreign.js", "HTTP/1.1 200 OK", "text/javascript",
"self.result = import(self.foreignModule).then(() => 'unexpected', error => error.name);".into(), Duration::ZERO),
("/foreign-module.js", "HTTP/1.1 200 OK", "text/javascript",
"export const value = 'private module';".into(), Duration::ZERO),
]).await;
let script = serde_json::to_string(&format!("{foreign_url}/foreign.js")).unwrap();
let module = serde_json::to_string(&format!("{foreign_url}/foreign-module.js")).unwrap();
let mut handle = spawn_worker_with_request_client(
format!(
r#"
self.foreignModule = {module};
try {{
importScripts({script});
result.then(value => {{ postMessage(value); close(); }});
}} catch (error) {{ postMessage('importScripts:' + error.name); close(); }}
"#
),
"http://127.0.0.1/worker/main.js".into(),
worker_test_request_client(),
);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(expect_post_json(message), r#""TypeError""#);
server.await.unwrap();
}
#[tokio::test]
async fn worker_importscripts_cross_origin_redirect_chain_stays_muted_after_returning() {
ensure_v8();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let worker_url = format!("http://{}", listener.local_addr().unwrap());
let redirect = format!(
"HTTP/1.1 302 Found\r\nLocation: {worker_url}/worker/creator.js\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
);
let (foreign_url, foreign_server) =
spawn_raw_path_response_http_server(vec![("/return.js", redirect, Duration::ZERO)]).await;
let server = tokio::spawn(async move {
let mut paths = Vec::new();
loop {
let (mut stream, _) = listener.accept().await.unwrap();
let request = read_http_request_head(&mut stream).await.unwrap();
let path = request
.lines()
.next()
.unwrap()
.split_whitespace()
.nth(1)
.unwrap();
paths.push(path.to_owned());
let (status, extra, body) = match path {
"/worker/redirect.js" => (
"302 Found",
format!("Location: {foreign_url}/return.js\r\n"),
"",
),
"/worker/creator.js" => (
"200 OK",
String::new(),
"self.result = import('./leaf.js').then(() => 'unexpected', error => error.name);",
),
"/worker/leaf.js" => (
"200 OK",
String::new(),
"export const value = 'unexpected';",
),
"/done" => ("200 OK", String::new(), "done"),
_ => panic!("unexpected importScripts request {path}"),
};
let response = format!(
"HTTP/1.1 {status}\r\n{extra}Content-Type: text/javascript\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
if path == "/done" {
return paths;
}
}
});
let mut handle = spawn_worker_with_request_client(
r#"
try {
importScripts('./redirect.js');
result.then(async value => { await fetch('/done'); postMessage(value); close(); });
} catch (error) { postMessage('importScripts:' + error.name); close(); }
"#
.into(),
format!("{worker_url}/worker/main.js"),
worker_test_request_client(),
);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(expect_post_json(message), r#""TypeError""#);
assert_eq!(
server.await.unwrap(),
["/worker/redirect.js", "/worker/creator.js", "/done"]
);
foreign_server.await.unwrap();
}
#[tokio::test]
async fn worker_importscripts_keeps_settings_base_separate_from_dynamic_import_base() {
ensure_v8();
let (base_url, server) = spawn_path_response_http_server(vec![
(
"/imported/creator.js",
"HTTP/1.1 200 OK",
"text/javascript",
"importScripts('./nested.js'); self.importLater = () => import('./leaf.js');".into(),
Duration::ZERO,
),
(
"/worker/nested.js",
"HTTP/1.1 200 OK",
"text/javascript",
"self.nested = 'worker';".into(),
Duration::ZERO,
),
(
"/imported/leaf.js",
"HTTP/1.1 200 OK",
"text/javascript",
"export const value = 'imported';".into(),
Duration::ZERO,
),
])
.await;
let mut handle = spawn_worker_with_request_client(
r#"
try {
importScripts('../imported/creator.js');
importLater().then(module => {
postMessage({nested, value: module.value}); close();
}, error => { postMessage({error: error.name}); close(); });
} catch (error) { postMessage({error: error.name}); close(); }
"#
.into(),
format!("{base_url}/worker/main.js"),
worker_test_request_client(),
);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(
expect_post_json(message),
r#"{"nested":"worker","value":"imported"}"#
);
server.await.unwrap();
}
#[tokio::test]
async fn worker_importscripts_cross_origin_sanitizes_import_base_but_not_settings_origin() {
ensure_v8();
let (worker_url, worker_server) = spawn_path_response_http_server(vec![
(
"/worker/nested.js",
"HTTP/1.1 200 OK",
"text/javascript",
"self.nested = 'worker';".into(),
Duration::ZERO,
),
(
"/worker/leaf.js",
"HTTP/1.1 200 OK",
"text/javascript",
"export const value = 'worker-module';".into(),
Duration::ZERO,
),
])
.await;
let absolute = serde_json::to_string(&format!("{worker_url}/worker/leaf.js")).unwrap();
let source = format!(
r#"
importScripts('./nested.js');
self.relativeImport = import('./leaf.js').then(() => 'unexpected', error => error.name);
self.absoluteImport = import({absolute}).then(module => module.value);
self.importLater = () => import('./later.js').then(() => 'unexpected', error => error.name);
"#
);
let (foreign_url, foreign_server) = spawn_path_response_http_server(vec![(
"/foreign/creator.js",
"HTTP/1.1 200 OK",
"text/javascript",
source,
Duration::ZERO,
)])
.await;
let foreign = serde_json::to_string(&format!("{foreign_url}/foreign/creator.js")).unwrap();
let mut handle = spawn_worker_with_request_client(
format!(
r#"
try {{
importScripts({foreign});
Promise.all([relativeImport, absoluteImport, importLater()]).then(values => {{
postMessage({{nested, values}}); close();
}}, error => {{ postMessage({{error: error.name}}); close(); }});
}} catch (error) {{ postMessage({{error: error.name}}); close(); }}
"#
),
format!("{worker_url}/worker/main.js"),
worker_test_request_client(),
);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(
expect_post_json(message),
r#"{"nested":"worker","values":["TypeError","worker-module","TypeError"]}"#
);
foreign_server.await.unwrap();
worker_server.await.unwrap();
}
#[tokio::test]
async fn worker_importscripts_cross_origin_async_errors_are_muted() {
ensure_v8();
let (foreign_url, server) = spawn_path_response_http_server(vec![(
"/foreign.js",
"HTTP/1.1 200 OK",
"text/javascript",
"setTimeout(() => { throw new Error('private message'); }, 0);".into(),
Duration::ZERO,
)])
.await;
let foreign = serde_json::to_string(&format!("{foreign_url}/foreign.js")).unwrap();
let mut handle = spawn_worker_with_request_client(
format!(
r#"
addEventListener('error', event => {{
postMessage({{message: event.message, filename: event.filename,
line: event.lineno, column: event.colno, errorIsNull: event.error === null}});
event.preventDefault(); close();
}});
importScripts({foreign});
"#
),
"http://127.0.0.1/worker/main.js".into(),
worker_test_request_client(),
);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(
expect_post_json(message),
r#"{"message":"Script error.","filename":"","line":0,"column":0,"errorIsNull":true}"#
);
server.await.unwrap();
}
#[tokio::test]
async fn worker_importscripts_cross_origin_rejections_are_not_reported() {
ensure_v8();
let (foreign_url, server) = spawn_path_response_http_server(vec![
("/foreign.js", "HTTP/1.1 200 OK", "text/javascript",
"Promise.reject('private immediate'); setTimeout(() => Promise.reject('private timer'), 0);".into(), Duration::ZERO),
]).await;
let foreign = serde_json::to_string(&format!("{foreign_url}/foreign.js")).unwrap();
let mut handle = spawn_worker_with_request_client(
format!(
r#"
const reasons = [];
addEventListener('unhandledrejection', event => {{
reasons.push(event.reason); event.preventDefault();
}});
try {{ importScripts({foreign}); }} catch (error) {{ reasons.push(error.name); }}
Promise.reject('public');
setTimeout(() => {{ postMessage(reasons); close(); }}, 0);
"#
),
"http://127.0.0.1/worker/main.js".into(),
worker_test_request_client(),
);
let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(expect_post_json(message), r#"["public"]"#);
server.await.unwrap();
}
@@ -1517,6 +1517,7 @@ fn service_worker_storage_apis_use_explicit_registration_storage_key() {
// ─── Basic tests ────────────────────────────────────────────────────
mod cors_redirects;
mod imported_scripts;
mod lazy_storage;
mod lifecycle;
mod modules;
@@ -4957,7 +4957,7 @@ async fn worker_importscripts_cross_origin_failures_throw_network_error() {
"/throw.js",
"HTTP/1.1 200 OK",
"application/javascript",
"globalThis.__crossOriginLoaded = true;".to_owned(),
"globalThis.__crossOriginLoaded = true; throw new Error('private message');".to_owned(),
Duration::ZERO,
),
])
@@ -5003,7 +5003,7 @@ async fn worker_importscripts_cross_origin_failures_throw_network_error() {
.expect("channel closed");
assert_eq!(
expect_post_json(msg),
r#"[{"name":"NetworkError","domException":true,"loaded":false},{"name":"NetworkError","domException":true,"loaded":false}]"#
r#"[{"name":"NetworkError","domException":true,"loaded":false},{"name":"NetworkError","domException":true,"loaded":true}]"#
);
script_server
.await
@@ -5011,7 +5011,7 @@ async fn worker_importscripts_cross_origin_failures_throw_network_error() {
}
#[tokio::test]
async fn worker_importscripts_redirect_to_cross_origin_failure_throws_network_error() {
async fn worker_importscripts_redirect_to_cross_origin_script_executes() {
ensure_v8();
let (cross_origin_base_url, script_server) = spawn_path_response_http_server(vec![(
"/throw.js",
@@ -5037,7 +5037,7 @@ async fn worker_importscripts_redirect_to_cross_origin_failure_throws_network_er
try {
importScripts("./redirect-throw.js");
postMessage({
name: "unexpected",
name: "ok",
domException: false,
loaded: globalThis.__redirectedCrossOriginLoaded === true,
});
@@ -5061,7 +5061,7 @@ async fn worker_importscripts_redirect_to_cross_origin_failure_throws_network_er
.expect("channel closed");
assert_eq!(
expect_post_json(msg),
r#"{"name":"NetworkError","domException":true,"loaded":false}"#
r#"{"name":"ok","domException":false,"loaded":true}"#
);
redirect_server
.await
@@ -6176,7 +6176,7 @@ async fn worker_classic_websocket_offline_reports_network_failure() {
}
#[tokio::test]
async fn worker_importscripts_websocket_resolves_against_imported_script_url() {
async fn worker_importscripts_websocket_resolves_against_worker_settings_url() {
ensure_v8();
let imported_script = r#"
const events = [];
@@ -6199,7 +6199,7 @@ async fn worker_importscripts_websocket_resolves_against_imported_script_url() {
)])
.await;
let websocket_base_url = base_url.replacen("http://", "ws://", 1);
let expected_url = format!("{websocket_base_url}/worker/imported/blocked/imported-ws");
let expected_url = format!("{websocket_base_url}/worker/blocked/imported-ws");
let loader =
ResourceRequestClient::new(&FetchConfig::default()).expect("worker importScripts loader");
let mut handle = spawn_worker_with_request_client_and_blocked_url_patterns(
@@ -6209,7 +6209,7 @@ async fn worker_importscripts_websocket_resolves_against_imported_script_url() {
.into(),
format!("{base_url}/worker/main.js"),
loader,
vec![format!("{websocket_base_url}/worker/imported/blocked/*")],
vec![format!("{websocket_base_url}/worker/blocked/*")],
);
let network = timeout(TIMEOUT, handle.recv())
+1 -1
View File
@@ -11463,7 +11463,7 @@ wait_until = "load"
timeout_ms = 5000
suite = "smoke"
tags = ["worker"]
notes = "Manual smoke port for dedicated worker importScripts rejecting direct cross-origin URLs and same-origin redirects that land on cross-origin targets with NetworkError. Same-origin importScripts injection and blob-url importScripts are covered separately."
notes = "Manual smoke port for dedicated worker importScripts executing direct and redirected cross-origin scripts, retaining their side effects while replacing their thrown exceptions with NetworkError. Same-origin importScripts injection and blob-url importScripts are covered separately."
[[test]]
id = "worker-importscripts-blob-basic"
@@ -58,9 +58,9 @@ promise_test(async function () {
directResult.domException,
"direct cross-origin importScripts should throw a DOMException",
);
assert_false(
assert_true(
directResult.loaded,
"direct cross-origin importScripts should not execute the target script",
"direct cross-origin script executes before its exception is replaced with NetworkError",
);
assert_equals(
@@ -72,12 +72,12 @@ promise_test(async function () {
redirectResult.domException,
"redirected cross-origin importScripts should throw a DOMException",
);
assert_false(
assert_true(
redirectResult.loaded,
"redirected cross-origin importScripts should not execute the target script",
"redirected cross-origin script executes before its exception is replaced with NetworkError",
);
} finally {
worker.terminate();
}
}, "Dedicated workers reject cross-origin importScripts loads, including redirected targets");
}, "Dedicated workers execute cross-origin imported scripts and mute their exceptions, including redirected targets");
</script>