fix(trusted-types): gate service worker registration URL

This commit is contained in:
ldm0
2026-08-28 19:56:06 +08:00
parent 39d474864b
commit af037eca2a
6 changed files with 177 additions and 18 deletions
@@ -3908,7 +3908,6 @@ trusted-types/HTMLElement-generic.html
trusted-types/ServiceWorkerContainer-register-from-DedicatedWorker.https.html
trusted-types/ServiceWorkerContainer-register-from-ServiceWorker.https.html
trusted-types/ServiceWorkerContainer-register-from-SharedWorker.https.html
trusted-types/ServiceWorkerContainer-register.https.html
trusted-types/TrustedType-AttributeNodes.html
trusted-types/TrustedTypePolicyFactory-constants.html
trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
@@ -7531,6 +7531,7 @@ trusted-types/SVGScriptElement-internal-slot.html
trusted-types/ServiceWorker-block-eval-function-constructor.https.html
trusted-types/ServiceWorker-eval.https.html
trusted-types/ServiceWorker-importScripts.https.html
trusted-types/ServiceWorkerContainer-register.https.html
trusted-types/SharedWorker-block-eval-function-constructor.html
trusted-types/SharedWorker-eval.html
trusted-types/SharedWorker-importScripts.html
@@ -340,18 +340,30 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
let promise = resolver.get_promise(scope);
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
let promise = resolver.get_promise(scope);
reject_service_worker_promise(scope, resolver, "failed to get native bridge");
rv.set(promise.into());
return;
};
let host = unsafe { &mut *host_ptr };
if args.length() == 0 {
throw_type_error(
scope,
"Failed to execute 'register' on 'ServiceWorkerContainer': 1 argument required, but only 0 present.",
);
return;
}
let owner = service_worker_container_owner_scope(scope, args.this());
let Some(request_context) = host.service_worker_window_request_context(owner) else {
let Some(request_context) =
(unsafe { &mut *host_ptr }).service_worker_window_request_context(owner)
else {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
let promise = resolver.get_promise(scope);
reject_service_worker_promise_with_type_error(
scope,
resolver,
@@ -360,7 +372,36 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
rv.set(promise.into());
return;
};
let Some(script_url) = service_worker_script_url(scope, request_context.document_url(), &args)
let Some(requirements) =
(unsafe { &*host_ptr }).trusted_types_for_script_requirements_for_owner(owner)
else {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
let promise = resolver.get_promise(scope);
reject_service_worker_promise_with_type_error(
scope,
resolver,
"service worker document is no longer current",
);
rv.set(promise.into());
return;
};
let Some(script) = crate::context_bootstrap::trusted_script_url_string_or_throw(
scope,
args.get(0),
requirements,
"ServiceWorkerContainer register",
"register",
) else {
return;
};
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
let promise = resolver.get_promise(scope);
let Some(script_url) =
resolve_service_worker_script_url(request_context.document_url(), &script)
else {
reject_service_worker_promise(
scope,
@@ -388,6 +429,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
};
let scope_url =
service_worker_scope_url(scope, request_context.document_url(), &script_url, &args);
let host = unsafe { &mut *host_ptr };
let Some(request_client) = host
.document_resource_loader_for_window_owner(request_context.owner().window_document_owner())
.map(|loader| loader.request_client().clone())
@@ -2036,16 +2078,8 @@ fn reject_service_worker_navigation_preload_state_error(
}
}
fn service_worker_script_url(
scope: &mut v8::PinScope<'_, '_>,
document_url: &url::Url,
args: &v8::FunctionCallbackArguments<'_>,
) -> Option<url::Url> {
let script = args
.get(0)
.to_string(scope)
.map(|value| value.to_rust_string_lossy(scope))?;
let mut script_url = document_url.join(&script).ok()?;
fn resolve_service_worker_script_url(document_url: &url::Url, script: &str) -> Option<url::Url> {
let mut script_url = document_url.join(script).ok()?;
script_url.set_fragment(None);
Some(script_url)
}
@@ -1373,6 +1373,29 @@ impl DocumentRuntime {
)
}
pub(crate) fn trusted_types_for_script_requirements_for_document(
&self,
document_handle: Option<DomHandle>,
response_policies: &[String],
report_only_policies: &[String],
reporting_endpoints: &ContentSecurityPolicyReportingEndpoints,
) -> TrustedTypesForScriptRequirements {
let enforced_policies = self
.document_content_security_policy_strings_for_optional_document(
document_handle,
response_policies,
reporting_endpoints,
);
let report_only_policies = document_response_content_security_policy_strings(
report_only_policies,
reporting_endpoints,
);
TrustedTypesForScriptRequirements::new(
document_policies_require_trusted_types_for_script(&enforced_policies),
document_policies_require_trusted_types_for_script(&report_only_policies),
)
}
pub(crate) fn requires_trusted_types_for_script(&self) -> bool {
let policies = self.document_content_security_policy_strings_for_optional_document(
Some(self.document_handle()),
@@ -72,6 +72,47 @@ impl JsContextHost {
Some(crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(&policy))
}
pub(crate) fn trusted_types_for_script_requirements_for_owner(
&self,
owner: OwnerDispatchScope,
) -> Option<TrustedTypesForScriptRequirements> {
match owner {
OwnerDispatchScope::Top => {
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(unsafe { &*self.runtime }.trusted_types_for_script_requirements())
}
OwnerDispatchScope::Child(handle) => {
let response_policies =
self.child_effective_response_content_security_policies(handle);
let report_only_policies =
self.child_effective_response_content_security_report_only_policies(handle);
let reporting_endpoints =
self.child_effective_content_security_reporting_endpoints(handle);
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
self.child_browsing_context_document_handle(handle),
&response_policies,
&report_only_policies,
&reporting_endpoints,
),
)
}
OwnerDispatchScope::LightweightPopup(popup_id) => {
let policy = self.lightweight_popup_policy_container(popup_id)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
self.lightweight_popup_document_handle(popup_id),
&policy.response_content_security_policies,
&policy.response_content_security_report_only_policies,
&policy.content_security_reporting_endpoints,
),
)
}
}
}
pub(crate) fn cross_origin_embedder_policy(
&self,
) -> crate::cross_origin_isolation::CrossOriginEmbedderPolicy {
@@ -842,3 +842,64 @@ fn script_execution_violation_outside_javascript_stack_avoids_v8_frame_probe() {
r#"{"blockedURI":"trusted-types-sink","sample":"HTMLScriptElement text|untrusted-source"}"#
);
}
#[test]
fn service_worker_register_gates_script_url_before_url_resolution() {
let mut vm = new_storage_test_vm("https://service-worker-register-trusted-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
const errorName = callback => {
try {
callback();
return "none";
} catch (error) {
return error && error.name;
}
};
const policy = trustedTypes.createPolicy("service-worker-register", {
createHTML: value => value,
createScriptURL: value => value
});
const blockedString = errorName(() => navigator.serviceWorker.register("worker.js"));
const blockedWrongType = errorName(() =>
navigator.serviceWorker.register(policy.createHTML("worker.js"))
);
const missing = errorName(() => navigator.serviceWorker.register());
const trustedPromise = navigator.serviceWorker.register(
policy.createScriptURL("http://[")
);
trustedPromise.catch(() => {});
const defaultCalls = [];
trustedTypes.createPolicy("default", {
createScriptURL: (value, type, sink) => {
defaultCalls.push([value, type, sink]);
return "http://[";
}
});
const defaultPromise = navigator.serviceWorker.register("worker.potato");
defaultPromise.catch(() => {});
return JSON.stringify({
blockedString,
blockedWrongType,
missing,
trustedPromise: trustedPromise instanceof Promise,
defaultPromise: defaultPromise instanceof Promise,
defaultCalls
});
})()
"#,
)
.expect("ServiceWorkerContainer.register TrustedScriptURL probe should evaluate");
assert_eq!(
result,
r#"{"blockedString":"TypeError","blockedWrongType":"TypeError","missing":"TypeError","trustedPromise":true,"defaultPromise":true,"defaultCalls":[["worker.potato","TrustedScriptURL","ServiceWorkerContainer register"]]}"#
);
}