fix(trusted-types): prepare script text before type

This commit is contained in:
ldm0
2026-09-16 22:02:05 +08:00
parent 3cf68904e2
commit 2f35db82b0
12 changed files with 201 additions and 35 deletions
@@ -3653,8 +3653,6 @@ trusted-types/eval-function-constructor-untrusted-arguments-and-applying-default
trusted-types/inheriting-csp-for-local-schemes.html
trusted-types/modify-attributes-in-callback.html
trusted-types/require-trusted-types-for-TypeError-belongs-to-the-global-object-realm.html
trusted-types/script-enforcement-010.html
trusted-types/script-enforcement-011.html
trusted-types/set-attributes-require-trusted-types-default-policy.html
trusted-types/set-attributes-require-trusted-types-no-default-policy-error-cases.html
trusted-types/set-attributes-require-trusted-types-no-default-policy.html
@@ -8049,6 +8049,8 @@ trusted-types/script-enforcement-004.html
trusted-types/script-enforcement-005.html
trusted-types/script-enforcement-006.html
trusted-types/script-enforcement-007.html
trusted-types/script-enforcement-010.html
trusted-types/script-enforcement-011.html
trusted-types/script-enforcement-012.html
trusted-types/script-enforcement-013.html
trusted-types/script-enforcement-014.html
@@ -2054,10 +2054,11 @@ pub(super) fn finish_runtime_script_start_candidate(
candidate: crate::mutation_coordinator::RuntimeScriptStartCandidate,
) {
let (node, host_script_handle) = candidate.into_parts();
let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else {
return;
};
match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(scope, runtime, plan) {
match unsafe { &mut *host_ptr }.plan_and_commit_current_main_runtime_script_start(
scope,
node,
&host_script_handle,
) {
Ok(Some(committed)) => {
execute_committed_inline_classic_script(runtime, scope, host_ptr, committed);
}
@@ -1,9 +1,10 @@
use super::*;
use crate::host::{
ModuleFailurePolicy, PreparedRuntimeScriptStartCommit, RuntimeScriptAdmission,
RuntimeScriptStartPlan, RuntimeScriptStartReservation, ScriptEventKind, ScriptEventTask,
ScriptHandleSource, cancel_runtime_script_start_admission, dispatch_script_event,
finish_runtime_script_start_admission, plan_script_start, prepare_runtime_script_start_commit,
RuntimeScriptStartPlan, RuntimeScriptStartReservation, RuntimeScriptTextPreparationReservation,
ScriptEventKind, ScriptEventTask, ScriptHandleSource, cancel_runtime_script_start_admission,
dispatch_script_event, finish_runtime_script_start_admission, plan_script_start,
prepare_runtime_script_start_commit,
};
use crate::page_task_queue::PostParseLifecycleWork;
use crate::planning::PreparedScript;
@@ -17,7 +18,7 @@ impl DocumentRuntime {
host_script_handle: &str,
) -> Option<RuntimeScriptStartPlan> {
let options = crate::host::ScriptElementLoaderOptions {
prepare_changed_empty_inline_source: self.requires_trusted_types_for_script(),
use_prepared_script_text: self.requires_trusted_types_for_script(),
..crate::host::ScriptElementLoaderOptions::with_scripting_enabled(
self.document_scripting_enabled(),
)
@@ -31,6 +32,26 @@ impl DocumentRuntime {
)
}
pub(crate) fn begin_runtime_script_text_preparation(
&mut self,
node: DomHandle,
host_script_handle: &str,
) -> Option<RuntimeScriptTextPreparationReservation> {
RuntimeScriptTextPreparationReservation::begin(
&self.dom_host,
self.script_lifecycle.scripts_mut(),
node,
host_script_handle,
)
}
pub(crate) fn release_runtime_script_text_preparation(
&mut self,
reservation: RuntimeScriptTextPreparationReservation,
) {
reservation.release(self.script_lifecycle.scripts_mut());
}
pub(crate) fn prepare_runtime_script_start_commit(
&mut self,
plan: RuntimeScriptStartPlan,
+3 -3
View File
@@ -40,9 +40,9 @@ pub(super) use self::scripts::{
CommittedInlineClassicScript, FailedDynamicScript, HostScriptScheduler, ModuleFailurePolicy,
PreparedRuntimeScriptStartCommit, QueuedScriptFailureKind, RuntimeScriptAdmission,
RuntimeScriptAdmissionPayload, RuntimeScriptPreparationContext, RuntimeScriptStartDecision,
RuntimeScriptStartPlan, RuntimeScriptStartReservation, ScriptElementLoader,
ScriptElementLoaderOptions, ScriptEventKind, ScriptEventTask, ScriptHandleSource,
ScriptPageTaskExecutionKind, apply_parser_script_element_state_transition,
RuntimeScriptStartPlan, RuntimeScriptStartReservation, RuntimeScriptTextPreparationReservation,
ScriptElementLoader, ScriptElementLoaderOptions, ScriptEventKind, ScriptEventTask,
ScriptHandleSource, ScriptPageTaskExecutionKind, apply_parser_script_element_state_transition,
apply_parser_script_element_state_without_execution,
begin_prepared_document_write_script_start, build_runtime_prepared_script,
cancel_runtime_script_start_admission, dispatch_script_event,
+14 -13
View File
@@ -93,10 +93,9 @@ pub(crate) struct ScriptElementLoaderOptions {
/// Valid scripts still commit their already-started state, but they must not
/// fetch, register an import map, or execute.
pub(crate) scripting_disabled: bool,
/// The caller will apply Trusted Types script-text preparation to the
/// returned inline source, so a changed empty source may still become
/// executable before the HTML empty-source check takes effect.
pub(crate) prepare_changed_empty_inline_source: bool,
/// Trusted Types script-text preparation has already updated the script's
/// internal slot. Read that slot before the empty-source and type checks.
pub(crate) use_prepared_script_text: bool,
}
impl ScriptElementLoaderOptions {
@@ -106,7 +105,7 @@ impl ScriptElementLoaderOptions {
suppress_force_async: false,
document_write_connected: false,
scripting_disabled: !scripting_enabled,
prepare_changed_empty_inline_source: false,
use_prepared_script_text: false,
}
}
}
@@ -347,14 +346,16 @@ pub(crate) fn decide_runtime_script_start(
let source = match script_src {
Some(source) => source,
None => {
let source = dom_host.dom().direct_text_content(node).unwrap_or_default();
let changed_since_trusted_source = dom_host
.node(node)
.and_then(Node::as_element)
.is_some_and(|element| element.script_text_internal_slot() != source);
if source.is_empty()
&& !(options.prepare_changed_empty_inline_source && changed_since_trusted_source)
{
let source = if options.use_prepared_script_text {
dom_host
.node(node)
.and_then(Node::as_element)
.map(|element| element.script_text_internal_slot().to_owned())
.unwrap_or_default()
} else {
dom_host.dom().direct_text_content(node).unwrap_or_default()
};
if source.is_empty() {
return RuntimeScriptStartDecision::Skip {
commit_start: false,
reason: Some(ScriptSkipReason::EmptyInlineScript),
@@ -162,6 +162,16 @@ impl PreparedRuntimeScriptStart {
return Ok(None);
}
self.execute_after_reservation(dom_host, scripts, host_script_handle)
}
#[cfg(test)]
fn execute_after_reservation(
self,
dom_host: &mut DomHost,
scripts: &mut HostScriptScheduler,
host_script_handle: &str,
) -> std::result::Result<Option<String>, String> {
let PreparedRuntimeScriptStart {
node,
preparation,
@@ -171,6 +181,7 @@ impl PreparedRuntimeScriptStart {
match decision {
RuntimeScriptStartDecision::Skip { commit_start, .. } => {
if !commit_start {
scripts.cancel_script_start(host_script_handle, node);
return Ok(None);
}
if !commit_runtime_script_start(
@@ -556,6 +567,53 @@ pub(super) fn commit_runtime_script_start(
true
}
/// Holds the per-element start lane while a Trusted Types default policy runs.
///
/// Policy callbacks can mutate the connected script and synchronously trigger
/// another preparation attempt. Reserving before entering JavaScript keeps
/// that nested attempt from preparing the same element a second time. The
/// reservation is released before the normal plan/commit pipeline resumes.
#[derive(Debug)]
pub(crate) struct RuntimeScriptTextPreparationReservation {
node: NativeNodeId,
host_script_handle: String,
}
impl RuntimeScriptTextPreparationReservation {
pub(crate) fn begin(
dom_host: &DomHost,
scripts: &mut HostScriptScheduler,
node: NativeNodeId,
host_script_handle: &str,
) -> Option<Self> {
let can_prepare = dom_host.is_connected(node)
&& dom_host
.node(node)
.and_then(|node| node.as_element())
.is_some_and(|element| {
element.is_script_element() && !element.script_already_started()
});
if !can_prepare {
return None;
}
scripts.register_script_handle_with_source(
host_script_handle,
node,
ScriptHandleSource::RuntimeOwned,
);
scripts
.reserve_script_start(host_script_handle, node)
.then(|| Self {
node,
host_script_handle: host_script_handle.to_owned(),
})
}
pub(crate) fn release(self, scripts: &mut HostScriptScheduler) {
scripts.cancel_script_start(&self.host_script_handle, self.node);
}
}
pub(crate) fn begin_prepared_document_write_script_start(
dom_host: &mut DomHost,
scripts: &mut HostScriptScheduler,
@@ -24,7 +24,30 @@ impl JsContextHost {
node: crate::document_runtime::DomHandle,
host_script_handle: &str,
) -> std::result::Result<Option<CommittedInlineClassicScript>, String> {
let runtime = unsafe { &mut *self.runtime };
let requires_trusted_types = self.requires_trusted_types_for_script(scope);
let host_ptr: *mut JsContextHost = self;
let runtime_ptr = self.runtime;
if requires_trusted_types {
let Some(reservation) = (unsafe { &mut *runtime_ptr })
.begin_runtime_script_text_preparation(node, host_script_handle)
else {
return Ok(None);
};
let source = unsafe { &*runtime_ptr }
.dom_host()
.dom()
.direct_text_content(node)
.unwrap_or_default();
let prepared = crate::native_bridge::element::prepare_trusted_script_text(
scope, host_ptr, node, &source,
);
unsafe { &mut *runtime_ptr }.release_runtime_script_text_preparation(reservation);
if prepared.is_none() {
return Ok(None);
}
}
let runtime = unsafe { &mut *runtime_ptr };
let Some(plan) = runtime.host_plan_script_start(node, host_script_handle) else {
return Ok(None);
};
@@ -61,8 +61,8 @@ use trusted_types::{
TrustedScriptElementSink, trusted_script_element_sink_string, trusted_script_url_sink_string,
};
pub(crate) use trusted_types::{
set_svg_animated_string_base_value, trusted_attribute_type_name_for_names,
trusted_property_type_name_for_names,
prepare_trusted_script_text, set_svg_animated_string_base_value,
trusted_attribute_type_name_for_names, trusted_property_type_name_for_names,
};
pub(crate) use forms::{
@@ -5,7 +5,7 @@ use crate::{
native_bridge::JsContextHost,
};
use super::trusted_types::trusted_script_source_for_execution;
use super::trusted_types::prepare_trusted_script_text;
pub(crate) fn prepare_inline_classic_frame_script_job_for_execution(
scope: &mut v8::PinScope<'_, '_>,
@@ -56,7 +56,7 @@ pub(crate) fn inline_script_source_for_execution(
source: &str,
request: ContentSecurityPolicyScriptElementRequest<'_>,
) -> Option<String> {
let source = trusted_script_source_for_execution(scope, host_ptr, script, source)?;
let source = prepare_trusted_script_text(scope, host_ptr, script, source)?;
let host = unsafe { &mut *host_ptr };
let Some(owner) = host.owner_dispatch_scope_for_node(script) else {
// A script with no live owner has no document policy container to
@@ -348,7 +348,7 @@ pub(crate) fn set_svg_animated_string_base_value<'s>(
Some(value)
}
pub(crate) fn trusted_script_source_for_execution(
pub(crate) fn prepare_trusted_script_text(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
@@ -374,7 +374,13 @@ pub(crate) fn trusted_script_source_for_execution(
if source == trusted_source {
return Some(source.to_owned());
}
crate::context_bootstrap::trusted_script_string_for_script_element_execution(
let source = crate::context_bootstrap::trusted_script_string_for_script_element_execution(
scope, source, sink,
)
)?;
// The Trusted Types integration updates the script-text slot before the
// HTML prepare algorithm reads the source and determines the script type.
let _ = unsafe { &mut *runtime_ptr }
.dom_host_mut()
.set_script_text_internal_slot(handle, &source);
Some(source)
}
@@ -1135,6 +1135,63 @@ fn trusted_types_default_policy_prepares_runtime_import_maps_before_registration
}
}
#[test]
fn trusted_types_default_policy_type_mutation_precedes_script_classification() {
let mut vm = new_storage_test_vm("https://script-type-trusted-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
const html = document.documentElement ||
document.appendChild(document.createElement("html"));
const body = document.body || html.appendChild(document.createElement("body"));
const svg = body.appendChild(
document.createElementNS("http://www.w3.org/2000/svg", "svg")
);
const source = `globalThis.__trustedTypeScriptKinds.push("CLASSIC");`;
globalThis.__trustedTypeScriptKinds = [];
const defaultCalls = [];
let script;
trustedTypes.createPolicy("default", {
createScript(value, type, sink) {
defaultCalls.push([type, sink]);
if (script.hasAttribute("type")) {
script.removeAttribute("type");
} else {
script.setAttribute("type", "text/plain");
}
return value;
}
});
for (const [namespace, parent] of [
["http://www.w3.org/1999/xhtml", body],
["http://www.w3.org/2000/svg", svg]
]) {
script = document.createElementNS(namespace, "script");
script.appendChild(document.createTextNode(source));
script.setAttribute("type", "module");
parent.appendChild(script);
script = document.createElementNS(namespace, "script");
script.appendChild(document.createTextNode(source));
parent.appendChild(script);
}
return JSON.stringify({ defaultCalls, runs: globalThis.__trustedTypeScriptKinds });
})()
"#,
)
.expect("Trusted Types script type mutation probe should evaluate");
assert_eq!(
result,
r#"{"defaultCalls":[["TrustedScript","HTMLScriptElement text"],["TrustedScript","HTMLScriptElement text"],["TrustedScript","SVGScriptElement text"],["TrustedScript","SVGScriptElement text"]],"runs":["CLASSIC","CLASSIC"]}"#
);
}
#[test]
fn inline_module_graph_roots_use_trusted_types_compliant_source() {
let mut vm = new_storage_test_vm("https://module-source-trusted-types.test/");
@@ -2158,7 +2215,6 @@ fn script_execution_violation_outside_javascript_stack_avoids_v8_frame_probe() {
const script = document.createElement("script");
script.id = "untrusted-script-source";
script.type = "application/json";
script.appendChild(document.createTextNode("untrusted-source"));
const root = document.body ||
(document.documentElement || document.appendChild(document.createElement("html")))
.appendChild(document.createElement("body"));