diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 470aeaf70d..4a4c0e1e28 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -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 diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 8195896785..87befa6296 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -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 diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 83862f19a9..628d9c10c7 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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; diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs index b2dc9cc59f..4880798d79 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs @@ -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>, ) -> Option { 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::::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 { + let object = v8::Local::::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, diff --git a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs index 3108166bac..c1829bd0dc 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs @@ -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); diff --git a/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/cache.rs b/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/cache.rs index 3c58f07751..f5029c46c7 100644 --- a/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/cache.rs +++ b/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/cache.rs @@ -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 { - 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> { - let lookup_name = attribute_lookup_name(scope, element, name); - let value = call_object_method( - scope, - element, - "getAttribute", - &[v8_string(scope, &lookup_name) - .map(Into::>::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> { - let namespace_value = namespace_uri - .and_then(|namespace| v8_string(scope, namespace)) - .map(Into::>::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) } diff --git a/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/instance/accessors.rs b/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/instance/accessors.rs index f942dc6c0c..7c6f0ba68b 100644 --- a/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/instance/accessors.rs +++ b/moli-renderer-v8/src/native_bridge/document/attributes/attr_object/instance/accessors.rs @@ -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::( 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; }; diff --git a/moli-renderer-v8/src/native_bridge/document/attributes/attribute_node.rs b/moli-renderer-v8/src/native_bridge/document/attributes/attribute_node.rs index 9b196dbe00..f564913f22 100644 --- a/moli-renderer-v8/src/native_bridge/document/attributes/attribute_node.rs +++ b/moli-renderer-v8/src/native_bridge/document/attributes/attribute_node.rs @@ -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> { - 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> { - 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); } } diff --git a/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/helpers.rs b/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/helpers.rs index 0f8032b0c2..8e75767480 100644 --- a/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/helpers.rs +++ b/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/helpers.rs @@ -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()); diff --git a/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/methods.rs b/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/methods.rs index e1f59a32ed..6d2a4302c6 100644 --- a/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/methods.rs +++ b/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/methods.rs @@ -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 { diff --git a/moli-renderer-v8/src/native_bridge/element/trusted_types.rs b/moli-renderer-v8/src/native_bridge/element/trusted_types.rs index 59cb15dd4c..773bfa1f37 100644 --- a/moli-renderer-v8/src/native_bridge/element/trusted_types.rs +++ b/moli-renderer-v8/src/native_bridge/element/trusted_types.rs @@ -265,6 +265,25 @@ pub(in crate::native_bridge) fn trusted_attribute_value_string<'s>( value: v8::Local<'s, v8::Value>, setter: TrustedAttributeSetter, ) -> Option { + // 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::( + 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::( diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index 8aaa39373d..f8b706de28 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -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; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types_attributes.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types_attributes.rs new file mode 100644 index 0000000000..5d7f6ea3e8 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types_attributes.rs @@ -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 = ``; + 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::(&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::(&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::(&result).unwrap(), + serde_json::json!([row, row]) + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs index 59dbb6c1c0..fa06260e0d 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs @@ -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]