mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 16:01:11 +00:00
fix(trusted-types): prepare script text before type
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user