fix(trusted-types): use node document policies for attribute sinks

Select attribute sink requirements, default policies, and report owners from the element's current node document while preserving the operation's exception realm. Avoid retrying native Attr mutations after conversion throws, and preserve native Attr namespace metadata and canonical cache identity.

Validated with cargo fmt, full-workspace Clippy, and all 17,730 nextest tests. The 219-case Trusted Types suite gains three passing cases and 36 passing subtests in both CLI and CDP, with no other semantic changes; all 13 DOM attribute/adoption regression cases remain passing.
This commit is contained in:
ldm0
2026-09-23 00:11:55 +08:00
parent c7dcb97237
commit b07a92136d
14 changed files with 446 additions and 230 deletions
@@ -3263,8 +3263,6 @@ svg/types/scripted/SVGLength-px.html
svg/types/scripted/SVGLength-rem.html
svg/types/scripted/SVGLength-rlh.html
svg/types/scripted/SVGLength-viewport.html
trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-TT-realm.html
trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-non-TT-realm.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
@@ -3274,7 +3272,6 @@ trusted-types/trusted-types-report-only.html
trusted-types/trusted-types-reporting-for-DedicatedWorker-ServiceWorkerContainer-register.https.html
trusted-types/trusted-types-reporting-for-ServiceWorker-ServiceWorkerContainer-register.https.html
trusted-types/trusted-types-reporting-for-SharedWorker-ServiceWorkerContainer-register.https.html
trusted-types/trusted-types-secondary-document.html
uievents/interface/click-event.htm
uievents/order-of-events/focus-events/focus-automated-blink-webkit.html
viewport/viewport-segments.html
@@ -8916,6 +8916,8 @@ trusted-types/Document-write-appending-line-feed.html
trusted-types/Document-write.html
trusted-types/Element-insertAdjacentHTML.html
trusted-types/Element-outerHTML.html
trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-TT-realm.html
trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-non-TT-realm.html
trusted-types/Element-setAttribute.html
trusted-types/Element-setAttributeNS.html
trusted-types/Element-toggleAttribute.html
@@ -9091,6 +9093,7 @@ trusted-types/trusted-types-reporting-for-Window-setTimeout-setInterval.html
trusted-types/trusted-types-reporting.html
trusted-types/trusted-types-sandbox-allow-scripts.html
trusted-types/trusted-types-sandbox-no-allow-scripts.html
trusted-types/trusted-types-secondary-document.html
trusted-types/trusted-types-source-file-path.html
trusted-types/trusted-types-svg-script-set-href.html
trusted-types/trusted-types-tojson.html
+3 -2
View File
@@ -438,10 +438,11 @@ pub(crate) use self::streams::{
#[cfg(test)]
pub(crate) use self::trusted_types::trusted_types_lazy_state_materialized;
pub(crate) use self::trusted_types::{
TrustedTypesCodeGenerationCheck, check_javascript_url_trusted_types,
TrustedTypeKind, TrustedTypesCodeGenerationCheck, check_javascript_url_trusted_types,
install_trusted_types_runtime_state, trusted_html_string_or_throw, trusted_html_value_string,
trusted_script_string_for_script_element_execution, trusted_script_string_or_type_error,
trusted_script_url_string_or_throw, trusted_types_code_generation_check,
trusted_script_url_string_or_throw, trusted_type_kind, trusted_type_string,
trusted_type_string_or_throw, trusted_types_code_generation_check,
};
pub(crate) use self::url_search_params_runtime::url_search_params_request_body;
pub(crate) use self::web_storage::install_storage_aliases_for_window;
@@ -220,7 +220,7 @@ struct TrustedTypePolicyObjectDeclaration<'scope> {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TrustedTypeKind {
pub(crate) enum TrustedTypeKind {
Html,
Script,
ScriptUrl,
@@ -308,7 +308,7 @@ pub(crate) fn trusted_script_url_string_or_throw<'s>(
requirements,
sink,
api_name,
TrustedTypeErrorKind::Type,
None,
)
}
@@ -326,7 +326,7 @@ pub(crate) fn trusted_html_string_or_throw<'s>(
requirements,
sink,
api_name,
TrustedTypeErrorKind::Type,
None,
)
}
@@ -351,7 +351,7 @@ pub(crate) fn trusted_script_string_or_type_error<'s>(
requirements,
sink,
api_name,
TrustedTypeErrorKind::Type,
None,
)
}
@@ -591,14 +591,16 @@ enum DefaultTrustedTypePolicyOutcome {
Exception,
}
fn trusted_type_string_or_throw<'s>(
/// An explicit global selects the sink's policy and report destination, without
/// changing the realm in which argument conversion and TypeError creation run.
pub(crate) fn trusted_type_string_or_throw<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
kind: TrustedTypeKind,
requirements: TrustedTypesForScriptRequirements,
sink: &str,
api_name: &'static str,
error_kind: TrustedTypeErrorKind,
policy_global: Option<v8::Local<'s, v8::Object>>,
) -> Option<String> {
if let Some(value) = trusted_type_string(scope, value, kind) {
return Some(value);
@@ -607,19 +609,30 @@ fn trusted_type_string_or_throw<'s>(
return js_value_to_string(scope, value);
}
let original = js_value_to_string(scope, value)?;
let default_policy = apply_default_trusted_type_policy_outcome(scope, &original, kind, sink);
let global = policy_global.unwrap_or_else(|| scope.get_current_context().global(scope));
let default_policy =
apply_default_trusted_type_policy_outcome_for_global(scope, global, &original, kind, sink);
let default_policy_rejected = match default_policy {
DefaultTrustedTypePolicyOutcome::Value(value) => return Some(value),
DefaultTrustedTypePolicyOutcome::Exception => return None,
DefaultTrustedTypePolicyOutcome::Unavailable => false,
DefaultTrustedTypePolicyOutcome::Rejected => true,
};
dispatch_trusted_types_sink_violation_event(scope, sink, &original);
if let Some(global) = policy_global {
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
unsafe { &mut *host_ptr }
.dispatch_trusted_types_sink_csp_violation_event_for_global_best_effort(
scope, host_ptr, global, sink, &original,
);
}
} else {
dispatch_trusted_types_sink_violation_event(scope, sink, &original);
}
if requirements.is_enforced() {
if default_policy_rejected {
throw_trusted_type_policy_result_error(scope, error_kind, kind);
throw_trusted_type_policy_result_error(scope, TrustedTypeErrorKind::Type, kind);
} else {
throw_trusted_type_error(scope, error_kind, api_name, kind, sink);
throw_trusted_type_error(scope, TrustedTypeErrorKind::Type, api_name, kind, sink);
}
None
} else {
@@ -652,6 +665,16 @@ fn apply_default_trusted_type_policy_outcome<'s>(
sink: &str,
) -> DefaultTrustedTypePolicyOutcome {
let global = scope.get_current_context().global(scope);
apply_default_trusted_type_policy_outcome_for_global(scope, global, input, kind, sink)
}
fn apply_default_trusted_type_policy_outcome_for_global<'s>(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
input: &str,
kind: TrustedTypeKind,
sink: &str,
) -> DefaultTrustedTypePolicyOutcome {
let Some(policy) = get_private_value(scope, global, TRUSTED_TYPES_DEFAULT_POLICY_SLOT)
.and_then(|policy| v8::Local::<v8::Object>::try_from(policy).ok())
else {
@@ -677,7 +700,21 @@ fn apply_default_trusted_type_policy_outcome<'s>(
}
}
fn trusted_type_string<'s>(
pub(crate) fn trusted_type_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Option<TrustedTypeKind> {
let object = v8::Local::<v8::Object>::try_from(value).ok()?;
let kind = get_private_value(scope, object, TRUSTED_TYPE_KIND_SLOT)?;
match kind.to_string(scope)?.to_rust_string_lossy(scope).as_str() {
"html" => Some(TrustedTypeKind::Html),
"script" => Some(TrustedTypeKind::Script),
"script-url" => Some(TrustedTypeKind::ScriptUrl),
_ => None,
}
}
pub(crate) fn trusted_type_string<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
kind: TrustedTypeKind,
@@ -55,6 +55,21 @@ fn policy_owner_dispatch_scope(scope: &mut v8::PinScope<'_, '_>) -> OwnerDispatc
OwnerDispatchScope::Top
}
fn policy_owner_dispatch_scope_for_global<'s>(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
) -> OwnerDispatchScope {
if let Some(handle) = get_private_value(scope, global, CHILD_BROWSING_CONTEXT_HANDLE_SLOT)
.and_then(|value| child_window_handle_from_marker_data(scope, value))
{
return OwnerDispatchScope::Child(handle);
}
if let Some(id) = crate::native_bridge::lightweight_popup_id_from_window(scope, global) {
return OwnerDispatchScope::LightweightPopup(id);
}
OwnerDispatchScope::Top
}
impl DocumentCspOutcome {
pub(crate) fn blocks_request(&self) -> bool {
matches!(self, Self::Blocked(_))
@@ -187,6 +202,17 @@ impl JsContextHost {
)
}
pub(crate) fn trusted_types_for_script_requirements_for_global<'s>(
&self,
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
) -> TrustedTypesForScriptRequirements {
self.trusted_types_for_script_requirements_for_owner(
policy_owner_dispatch_scope_for_global(scope, global),
)
.unwrap_or_default()
}
fn trusted_types_sink_csp_violations_for_owner(
&self,
owner: OwnerDispatchScope,
@@ -1018,8 +1044,23 @@ impl JsContextHost {
sink: &str,
sample: &str,
) {
let owner = policy_owner_dispatch_scope(scope);
self.dispatch_trusted_types_sink_csp_violation_event_with_location_best_effort(
scope, host_ptr, sink, sample, true,
scope, host_ptr, owner, sink, sample, true,
);
}
pub(crate) fn dispatch_trusted_types_sink_csp_violation_event_for_global_best_effort<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
host_ptr: *mut JsContextHost,
global: v8::Local<'s, v8::Object>,
sink: &str,
sample: &str,
) {
let owner = policy_owner_dispatch_scope_for_global(scope, global);
self.dispatch_trusted_types_sink_csp_violation_event_with_location_best_effort(
scope, host_ptr, owner, sink, sample, true,
);
}
@@ -1033,8 +1074,9 @@ impl JsContextHost {
sink: &str,
sample: &str,
) {
let owner = policy_owner_dispatch_scope(scope);
self.dispatch_trusted_types_sink_csp_violation_event_with_location_best_effort(
scope, host_ptr, sink, sample, false,
scope, host_ptr, owner, sink, sample, false,
);
}
@@ -1059,6 +1101,7 @@ impl JsContextHost {
&mut self,
scope: &mut v8::PinScope<'s, '_>,
host_ptr: *mut JsContextHost,
owner: OwnerDispatchScope,
sink: &str,
sample: &str,
capture_current_script_location: bool,
@@ -1066,7 +1109,6 @@ impl JsContextHost {
let source_location = (capture_current_script_location && !self.active_inspector_dispatch)
.then(|| current_script_violation_location(scope))
.flatten();
let owner = policy_owner_dispatch_scope(scope);
for mut violation in self.trusted_types_sink_csp_violations_for_owner(owner, sink, sample) {
if let Some((source_file, line_number, column_number)) = &source_location {
violation.source_file.clone_from(source_file);
@@ -1,31 +1,10 @@
use super::super::attribute_node::{native_attr_object_by_name, native_attr_object_by_namespace};
use super::instance::attr_current_value;
use super::*;
use crate::native_bridge::node_runtime_and_handle_from_object;
use crate::util::{get_private_value, new_null_prototype_object, set_private_value};
const ATTR_OBJECT_CACHE_SLOT: &str = "__moliAttrObjectCache";
fn live_native_attribute_lookup_name<'s>(
scope: &mut v8::PinScope<'s, '_>,
element: v8::Local<'s, v8::Object>,
name: &str,
) -> Option<String> {
let (runtime_ptr, handle) = node_runtime_and_handle_from_object(scope, element).ok()?;
unsafe { &*runtime_ptr }
.dom_host()
.dom()
.normalized_attribute_name(handle, name)
}
fn attribute_lookup_name<'s>(
scope: &mut v8::PinScope<'s, '_>,
element: v8::Local<'s, v8::Object>,
name: &str,
) -> String {
live_native_attribute_lookup_name(scope, element, name)
.unwrap_or_else(|| detached_attribute_name(scope, element, name))
}
fn ensure_object_cache<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -276,42 +255,7 @@ pub(crate) fn live_get_attribute_node_object<'s>(
element: v8::Local<'s, v8::Object>,
name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
let lookup_name = attribute_lookup_name(scope, element, name);
let value = call_object_method(
scope,
element,
"getAttribute",
&[v8_string(scope, &lookup_name)
.map(Into::<v8::Local<'_, v8::Value>>::into)
.unwrap_or_else(|| v8::String::empty(scope).into())],
)?;
if value.is_null_or_undefined() {
return None;
}
let cache = live_attr_cache_object(scope, element)?;
let mut attr = object_property_as_object(scope, cache, &lookup_name)
.or_else(|| cached_attr_by_name(scope, cache, &lookup_name));
if attr.is_none() {
attr = new_attr_object(
scope,
&lookup_name,
"",
Some(element),
None,
None,
None,
&lookup_name,
);
if let Some(attr_object) = attr {
set_attr_cache_entry(scope, cache, &lookup_name, attr_object);
}
}
let attr = attr?;
if let Some(state) = attr_state_object(scope, attr) {
let _ = state.set(scope, v8str(scope, "ownerElement").into(), element.into());
let _ = state.set(scope, v8str(scope, "value").into(), value);
}
Some(attr)
native_attr_object_by_name(scope, element, name)
}
pub(in crate::native_bridge) fn live_get_attribute_node_ns_object<'s>(
@@ -320,41 +264,5 @@ pub(in crate::native_bridge) fn live_get_attribute_node_ns_object<'s>(
namespace_uri: Option<&str>,
local_name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
let namespace_value = namespace_uri
.and_then(|namespace| v8_string(scope, namespace))
.map(Into::<v8::Local<'_, v8::Value>>::into)
.unwrap_or_else(|| v8::null(scope).into());
let local_name_value = v8_string(scope, local_name)?;
let value = call_object_method(
scope,
element,
"getAttributeNS",
&[namespace_value, local_name_value.into()],
)?;
if value.is_null_or_undefined() {
return None;
}
let cache = live_attr_cache_object(scope, element)?;
let namespace_key = namespace_attr_cache_key(namespace_uri, local_name);
let attr = object_property_as_object(scope, cache, &namespace_key)
.or_else(|| cached_attr_by_namespace(scope, cache, namespace_uri, local_name))
.or_else(|| {
let attr = new_attr_object(
scope,
local_name,
"",
Some(element),
None,
namespace_uri,
None,
local_name,
)?;
set_attr_cache_entry(scope, cache, &namespace_key, attr);
Some(attr)
})?;
if let Some(state) = attr_state_object(scope, attr) {
let _ = state.set(scope, v8str(scope, "ownerElement").into(), element.into());
let _ = state.set(scope, v8str(scope, "value").into(), value);
}
Some(attr)
native_attr_object_by_namespace(scope, element, namespace_uri, local_name)
}
@@ -142,6 +142,14 @@ pub(super) fn attr_instance_value_setter<'a>(
args: v8::PropertyCallbackArguments<'a>,
_rv: v8::ReturnValue<'_, ()>,
) {
// V8 native data-property callbacks enter in the caller's context, unlike
// Web IDL setter functions. Keep conversion errors in the Attr's realm;
// the sink policy is separately selected from its owner's node document.
let attr = args.holder();
let context = attr
.get_creation_context(scope)
.unwrap_or_else(|| scope.get_current_context());
let scope: &mut v8::PinScope<'a, '_> = &mut v8::ContextScope::new(scope, context);
let string_value = match webidl::convert::<webidl::DomString>(
scope,
value,
@@ -153,7 +161,6 @@ pub(super) fn attr_instance_value_setter<'a>(
return;
}
};
let attr = args.holder();
let Some(state) = attr_state_object(scope, attr) else {
return;
};
@@ -8,6 +8,7 @@ use crate::native_bridge::element::{
set_live_element_attribute_ns_appending_to_current_reaction_queue,
trusted_attribute_string_value,
};
use crate::native_bridge::node::node_runtime_and_handle_from_object_or_detached;
use crate::native_bridge::node_runtime_and_handle_from_object;
use crate::webidl;
@@ -198,20 +199,14 @@ pub(in crate::native_bridge) fn detached_set_attribute_node_method_callback<'a>(
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if let Some((runtime_ptr, _)) = detached_native_element_runtime_and_handle(scope, args.this()) {
let mut handled = false;
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
if let Some(value) = detached_native_set_attribute_node(scope, args.this(), args.get(0))
{
rv.set(value);
handled = true;
}
});
if handled {
return;
}
}
if let Some(value) = detached_native_set_attribute_node(scope, args.this(), args.get(0)) {
rv.set(value);
// A native receiver was handled even when conversion threw. Retrying
// would invoke the default policy twice and can swallow its exception.
return;
}
match detached_method_forward(scope, args, "__setAttributeNodeForLiveElement") {
@@ -637,24 +632,18 @@ fn live_native_attr_object_from_metadata<'s>(
let cache = live_attr_cache_object(scope, element)?;
let namespace_key =
namespace_attr_cache_key(metadata.namespace_uri.as_deref(), &metadata.local_name);
let attr = object_property_as_object(scope, cache, &namespace_key)
.or_else(|| {
live_attr_metadata_can_alias_qualified_name(metadata)
.then(|| object_property_as_object(scope, cache, &metadata.name))
.flatten()
})
.or_else(|| {
new_attr_object(
scope,
&metadata.name,
&metadata.value,
Some(element),
None,
metadata.namespace_uri.as_deref(),
metadata.prefix.as_deref(),
&metadata.local_name,
)
})?;
let attr = object_property_as_object(scope, cache, &namespace_key).or_else(|| {
new_attr_object(
scope,
&metadata.name,
&metadata.value,
Some(element),
None,
metadata.namespace_uri.as_deref(),
metadata.prefix.as_deref(),
&metadata.local_name,
)
})?;
if let Some(state) = attr_state_object(scope, attr) {
attach_attr_node(scope, state, element, &metadata.value);
}
@@ -777,30 +766,32 @@ fn nullable_state_string<'s>(
.filter(|value| !value.is_empty())
}
fn native_attr_object_by_name<'s>(
pub(in crate::native_bridge::document) fn native_attr_object_by_name<'s>(
scope: &mut v8::PinScope<'s, '_>,
element: v8::Local<'s, v8::Object>,
name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
read_detached_native_attribute_snapshot(scope, element)?
.into_iter()
.find(|attribute| attribute.name == name)
.and_then(|attribute| native_attr_object_from_snapshot(scope, element, &attribute))
let (runtime_ptr, handle) =
node_runtime_and_handle_from_object_or_detached(scope, element).ok()?;
let metadata = live_native_attribute_metadata_for_name(unsafe { &*runtime_ptr }, handle, name)?;
live_native_attr_object_from_metadata(scope, element, &metadata)
}
fn native_attr_object_by_namespace<'s>(
pub(in crate::native_bridge::document) fn native_attr_object_by_namespace<'s>(
scope: &mut v8::PinScope<'s, '_>,
element: v8::Local<'s, v8::Object>,
namespace_uri: Option<&str>,
local_name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
read_detached_native_attribute_snapshot(scope, element)?
.into_iter()
.find(|attribute| {
attribute.namespace_uri.as_deref() == namespace_uri
&& attribute.local_name == local_name
})
.and_then(|attribute| native_attr_object_from_snapshot(scope, element, &attribute))
let (runtime_ptr, handle) =
node_runtime_and_handle_from_object_or_detached(scope, element).ok()?;
let metadata = live_native_attribute_metadata_for_namespace(
unsafe { &*runtime_ptr },
handle,
namespace_uri,
local_name,
)?;
live_native_attr_object_from_metadata(scope, element, &metadata)
}
pub(in crate::native_bridge::document) fn native_attr_object_from_snapshot<'s>(
@@ -811,24 +802,18 @@ pub(in crate::native_bridge::document) fn native_attr_object_from_snapshot<'s>(
let cache = live_attr_cache_object(scope, element)?;
let namespace_key =
namespace_attr_cache_key(attribute.namespace_uri.as_deref(), &attribute.local_name);
let attr = object_property_as_object(scope, cache, &namespace_key)
.or_else(|| {
native_attr_can_alias_qualified_name(attribute)
.then(|| object_property_as_object(scope, cache, &attribute.name))
.flatten()
})
.or_else(|| {
new_attr_object(
scope,
&attribute.name,
&attribute.value,
Some(element),
None,
attribute.namespace_uri.as_deref(),
attribute.prefix.as_deref(),
&attribute.local_name,
)
})?;
let attr = object_property_as_object(scope, cache, &namespace_key).or_else(|| {
new_attr_object(
scope,
&attribute.name,
&attribute.value,
Some(element),
None,
attribute.namespace_uri.as_deref(),
attribute.prefix.as_deref(),
&attribute.local_name,
)
})?;
if let Some(state) = attr_state_object(scope, attr) {
attach_attr_node(scope, state, element, &attribute.value);
}
@@ -859,12 +844,7 @@ fn cache_attached_attr_node<'s>(
let namespace_key =
namespace_attr_cache_key(metadata.namespace_uri.as_deref(), &metadata.local_name);
set_attr_cache_entry(scope, cache, &namespace_key, attr);
if metadata.namespace_uri.is_none()
|| metadata
.prefix
.as_deref()
.is_some_and(|prefix| !prefix.is_empty())
{
if live_attr_metadata_can_alias_qualified_name(metadata) {
set_attr_cache_entry(scope, cache, &metadata.name, attr);
}
}
@@ -193,12 +193,10 @@ fn indexed_attribute_object<'s>(
let cache_key = indexed_attribute_cache_key(&attribute);
let namespace_key =
namespace_attr_cache_key(attribute.namespace_uri.as_deref(), &attribute.local_name);
let name_alias = indexed_attribute_can_alias_qualified_name(&attribute)
.then(|| object_property_as_object(scope, cache, &attribute.name))
.flatten();
// Qualified names are not unique across namespaces. Only canonical keys
// identify an Attr; the name alias is an exposed lookup projection.
if let Some(attr) = object_property_as_object(scope, cache, &cache_key)
.or_else(|| object_property_as_object(scope, cache, &namespace_key))
.or(name_alias)
{
if let Some(state) = attr_state_object(scope, attr) {
let _ = state.set(scope, v8str(scope, "ownerElement").into(), element.into());
@@ -78,19 +78,11 @@ pub(in crate::native_bridge::document) fn named_node_map_set_named_item_method_c
return;
};
if let Some((runtime_ptr, _)) = detached_native_element_runtime_and_handle(scope, element) {
let mut handled = false;
crate::custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
if let Some(value) = detached_native_set_attribute_node(scope, element, args.get(0)) {
rv.set(value);
handled = true;
}
});
if handled {
return;
}
}
if let Some(value) = detached_native_set_attribute_node(scope, element, args.get(0)) {
rv.set(value);
return;
}
let Some(value) = live_set_attribute_node(scope, element, args.get(0)) else {
@@ -177,19 +169,11 @@ pub(in crate::native_bridge::document) fn named_node_map_set_named_item_ns_metho
return;
};
if let Some((runtime_ptr, _)) = detached_native_element_runtime_and_handle(scope, element) {
let mut handled = false;
crate::custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
if let Some(value) = detached_native_set_attribute_node(scope, element, args.get(0)) {
rv.set(value);
handled = true;
}
});
if handled {
return;
}
}
if let Some(value) = detached_native_set_attribute_node(scope, element, args.get(0)) {
rv.set(value);
return;
}
let Some(value) = live_set_attribute_node(scope, element, args.get(0)) else {
@@ -265,6 +265,25 @@ pub(in crate::native_bridge) fn trusted_attribute_value_string<'s>(
value: v8::Local<'s, v8::Value>,
setter: TrustedAttributeSetter,
) -> Option<String> {
// Web IDL converts the union before the DOM algorithm observes the node
// document. A user-defined toString can adopt the element into another realm.
let input_kind = crate::context_bootstrap::trusted_type_kind(scope, value);
let value = if input_kind.is_some() || value.is_string() {
value
} else {
let value = match crate::webidl::convert::<crate::webidl::DomString>(
scope,
value,
setter.conversion_context(),
) {
Ok(value) => value.0,
Err(error) => {
crate::webidl::throw_error(scope, &error);
return None;
}
};
crate::util::v8_string(scope, &value)?.into()
};
let sink = runtime_and_handle.and_then(|(runtime_ptr, handle)| {
let element = unsafe { &*runtime_ptr }
.dom_host()
@@ -276,40 +295,35 @@ pub(in crate::native_bridge) fn trusted_attribute_value_string<'s>(
attribute_namespace,
local_name,
)?;
Some((runtime_ptr, sink))
Some((runtime_ptr, handle, sink))
});
if let Some((runtime_ptr, sink)) = sink {
let requirements = unsafe { &*runtime_ptr }.trusted_types_for_script_requirements(scope);
return match sink {
TrustedAttributeSink::Html(sink) => {
crate::context_bootstrap::trusted_html_string_or_throw(
scope,
value,
requirements,
sink,
setter.api_name(),
)
}
TrustedAttributeSink::Script(sink) => {
crate::context_bootstrap::trusted_script_string_or_type_error(
scope,
value,
requirements,
&sink,
setter.api_name(),
)
}
TrustedAttributeSink::ScriptUrl(sink) => {
crate::context_bootstrap::trusted_script_url_string_or_throw(
scope,
value,
requirements,
sink,
setter.api_name(),
)
}
if let Some((runtime_ptr, handle, sink)) = sink {
let context =
super::super::node::node_owner_document_relevant_context(scope, runtime_ptr, handle)
.unwrap_or_else(|| scope.get_current_context());
let global = context.global(scope);
let requirements = unsafe { &*runtime_ptr }
.trusted_types_for_script_requirements_for_global(scope, global);
use crate::context_bootstrap::TrustedTypeKind;
let (kind, sink) = match &sink {
TrustedAttributeSink::Html(sink) => (TrustedTypeKind::Html, *sink),
TrustedAttributeSink::Script(sink) => (TrustedTypeKind::Script, sink.as_str()),
TrustedAttributeSink::ScriptUrl(sink) => (TrustedTypeKind::ScriptUrl, *sink),
};
return crate::context_bootstrap::trusted_type_string_or_throw(
scope,
value,
kind,
requirements,
sink,
setter.api_name(),
Some(global),
);
}
if let Some(kind) = input_kind {
return crate::context_bootstrap::trusted_type_string(scope, value, kind);
}
match crate::webidl::convert::<crate::webidl::DomString>(
@@ -42,6 +42,7 @@ mod structured_clone;
mod transferable_streams;
mod traversal;
mod trusted_types;
mod trusted_types_attributes;
mod web_audio;
mod webrtc;
mod webrtc_events;
@@ -0,0 +1,244 @@
use super::*;
#[tokio::test]
async fn adopted_attribute_sinks_use_the_node_document_global_and_preserve_exception_realms() {
let mut vm = new_storage_test_vm("https://adopted-attribute-types.test/");
vm.eval(r#"
(() => {
const root = document.documentElement || document.appendChild(document.createElement("html"));
const body = document.body || root.appendChild(document.createElement("body"));
const frame = document.createElement("iframe");
frame.id = "tt-frame";
frame.srcdoc = `<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'"><body></body>`;
body.appendChild(frame);
})()
"#).expect("Trusted Types child setup should evaluate");
run_child_navigation_commit_and_host_load_for_test(&mut vm, "TT child should commit").await;
let result = vm.eval(r#"
(() => {
const child = document.getElementById("tt-frame").contentWindow;
const childDoc = child.document;
const secondary = childDoc.implementation.createHTMLDocument("");
const explicit = trustedTypes.createPolicy("pass", { createScript: value => value });
const setters = [
(element, value) => element.setAttribute("onclick", value),
(element, value) => element.setAttributeNS(null, "onclick", value),
...["setAttributeNode", "setAttributeNodeNS", "setNamedItem", "setNamedItemNS"].map(method =>
(element, value) => {
const attr = document.createAttribute("onclick");
attr.value = value;
return method.startsWith("setNamed") ? element.attributes[method](attr) : element[method](attr);
}),
...["value", "nodeValue", "textContent"].map(property =>
(element, value) => { element.getAttributeNode("onclick")[property] = value; })
];
globalThis.__attributeReports = { top: [], child: [] };
// Observe the receiving global independently of the event's DOM target.
window.addEventListener("securitypolicyviolation", event => __attributeReports.top.push(event.sample));
child.addEventListener("securitypolicyviolation", event => __attributeReports.child.push(event.sample));
const moveAndSet = (source, target, set, index) => {
const element = source.createElement("button");
element.setAttribute("onclick", explicit.createScript("initial"));
target.adoptNode(element);
// Like the adoption WPT, derive the exception realm from the exposed
// receiver, without assuming that adoption keeps the original wrapper realm.
const receiver = index < 6 ? element : element.getAttributeNode("onclick");
const expectedError = new receiver.constructor.constructor(explicit.createScript("return TypeError"))();
let outcome = "none";
try { set(element, "input"); } catch (error) {
outcome = `${error.name}:${error instanceof expectedError}`;
}
return [outcome, element.ownerDocument === target, element.getAttribute("onclick")];
};
const intoPlain = setters.map((set, index) => moveAndSet(childDoc, document, set, index));
const intoEnforced = setters.map((set, index) => moveAndSet(document, childDoc, set, index));
const intoSecondary = setters.map((set, index) => moveAndSet(document, secondary, set, index));
const stringConversions = [false, true].map(namespaced => {
const element = document.createElement("button");
let converted = 0;
const value = { toString() { converted++; childDoc.adoptNode(element); return "converted-input"; }};
let blocked = false;
try {
if (namespaced) element.setAttributeNS(null, "onclick", value);
else element.setAttribute("onclick", value);
} catch (error) { blocked = error instanceof TypeError; }
return [blocked, converted, element.ownerDocument === childDoc, element.hasAttribute("onclick")];
});
const calls = [];
trustedTypes.createPolicy("default", { createScript: value => { calls.push("top"); return `top-${value}`; }});
child.trustedTypes.createPolicy("default", { createScript: value => { calls.push("child"); return `child-${value}`; }});
const defaulted = setters.map((set, index) => moveAndSet(document, childDoc, set, index));
const secondaryDefaulted = setters.map((set, index) => moveAndSet(document, secondary, set, index));
return JSON.stringify({ intoPlain, intoEnforced, intoSecondary, stringConversions, defaulted, secondaryDefaulted, calls });
})()
"#).expect("adopted attribute sink checks should evaluate");
let allowed = vec![serde_json::json!(["none", true, "input"]); 9];
let blocked = vec![serde_json::json!(["TypeError:true", true, "initial"]); 9];
let converted = vec![serde_json::json!(["none", true, "child-input"]); 9];
assert_eq!(
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
serde_json::json!({
"intoPlain": allowed, "intoEnforced": blocked, "intoSecondary": blocked,
"stringConversions": [[true, 1, true, false], [true, 1, true, false]],
"defaulted": converted, "secondaryDefaulted": converted, "calls": vec!["child"; 18]
})
);
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm);
assert_eq!(
vm.eval("JSON.stringify([__attributeReports.top.length, __attributeReports.child.length])")
.unwrap(),
"[0,20]"
);
}
#[test]
fn secondary_document_attribute_nodes_enforce_trusted_types_without_retrying() {
let mut vm = new_storage_test_vm("https://secondary-document-attribute-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
const secondary = document.implementation.createHTMLDocument("");
const setters = [
(element, attr) => element.setAttributeNode(attr),
(element, attr) => element.setAttributeNodeNS(attr),
(element, attr) => element.attributes.setNamedItem(attr),
(element, attr) => element.attributes.setNamedItemNS(attr)
];
const rejected = setters.map(set => {
const element = secondary.createElement("button");
const attr = secondary.createAttribute("onclick");
attr.value = "blocked";
let rejected = false;
try { set(element, attr); } catch (error) { rejected = error instanceof TypeError; }
return [rejected, attr.ownerElement === null, element.hasAttribute("onclick")];
});
const calls = [];
const exception = new RangeError("policy callback");
trustedTypes.createPolicy("default", { createScript(value) {
calls.push(value);
if (value === "throw") throw exception;
return `safe-${value}`;
}});
const accepted = setters.map(set => {
const element = secondary.createElement("button");
const attr = secondary.createAttribute("onclick");
attr.value = "input";
const old = set(element, attr);
return [old === null, attr.ownerElement === element,
element.getAttribute("onclick"), element.getAttributeNode("onclick") === attr];
});
const abrupt = setters.map(set => {
const element = secondary.createElement("button");
const attr = secondary.createAttribute("onclick");
attr.value = "throw";
let preserved = false;
try { set(element, attr); } catch (error) { preserved = error === exception; }
return [preserved, attr.ownerElement === null, element.hasAttribute("onclick")];
});
return JSON.stringify({ rejected, accepted, abrupt, calls });
})()
"#,
)
.expect("secondary document attribute node checks should evaluate");
assert_eq!(
result,
r#"{"rejected":[[true,true,false],[true,true,false],[true,true,false],[true,true,false]],"accepted":[[true,true,"safe-input",true],[true,true,"safe-input",true],[true,true,"safe-input",true],[true,true,"safe-input",true]],"abrupt":[[true,true,false],[true,true,false],[true,true,false],[true,true,false]],"calls":["input","input","input","input","throw","throw","throw","throw"]}"#
);
}
#[test]
fn attribute_node_lookup_preserves_native_namespaces_and_shared_identity() {
let mut vm = new_storage_test_vm("https://attribute-node-namespace.test/");
let result = vm
.eval(
r#"
(() => {
const ns = "http://www.w3.org/1999/xlink";
const secondary = document.implementation.createHTMLDocument("");
const results = [];
for (const doc of [document, secondary]) {
for (const nsFirst of [false, true]) {
const element = doc.createElementNS("http://www.w3.org/2000/svg", "script");
element.setAttributeNS(ns, "xlink:href", "initial");
element.getAttribute = element.getAttributeNS = () => { throw new Error("overridden getter"); };
const attr = nsFirst ? element.getAttributeNodeNS(ns, "href")
: element.getAttributeNode("xlink:href");
const identity = attr === element.getAttributeNodeNS(ns, "href") &&
attr === element.getAttributeNode("xlink:href") &&
attr === element.attributes.item(0) &&
attr === element.attributes.getNamedItem("xlink:href") &&
attr === element.attributes.getNamedItemNS(ns, "href");
const metadata = [attr.name, attr.localName, attr.prefix, attr.namespaceURI];
delete element.getAttribute;
delete element.getAttributeNS;
element.removeAttributeNode(attr);
element.setAttributeNode(attr);
attr.value = "changed";
results.push([metadata, identity, attr.ownerElement === element,
element.getAttributeNS(ns, "href"), element.attributes.length]);
}
}
return JSON.stringify(results);
})()
"#,
)
.expect("attribute node namespace checks should evaluate");
let expected = serde_json::json!([
[
"xlink:href",
"href",
"xlink",
"http://www.w3.org/1999/xlink"
],
true,
true,
"changed",
1
]);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
serde_json::json!([expected, expected, expected, expected])
);
}
#[test]
fn attribute_node_cache_distinguishes_equal_qualified_names_in_different_namespaces() {
let mut vm = new_storage_test_vm("https://attribute-node-cache-namespace.test/");
let result = vm
.eval(
r#"
(() => {
const secondary = document.implementation.createHTMLDocument("");
return JSON.stringify([document, secondary].map(doc => {
const element = doc.createElementNS("http://www.w3.org/2000/svg", "g");
element.setAttributeNS("urn:first", "same:name", "first");
element.setAttributeNS("urn:second", "same:name", "second");
const first = element.getAttributeNodeNS("urn:first", "name");
const second = element.getAttributeNodeNS("urn:second", "name");
const byName = element.getAttributeNode("same:name");
const indexed = [element.attributes[0], element.attributes[1]];
second.value = "updated";
return [first !== second, byName === first, indexed[0] === first, indexed[1] === second,
first.namespaceURI, second.namespaceURI, first.value, second.value];
}));
})()
"#,
)
.expect("namespace cache identity checks should evaluate");
let row = serde_json::json!([
true,
true,
true,
true,
"urn:first",
"urn:second",
"first",
"updated"
]);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
serde_json::json!([row, row])
);
}
@@ -7073,7 +7073,7 @@ fn detached_document_adopts_live_namespaced_attributes() {
assert_eq!(
result,
"native||value|value|true|data-real,a:flag|flag||urn:attr|flag|value"
"native||value|value|true|data-real,a:flag|a:flag|a|urn:attr|flag|value"
);
}
#[test]