diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index b27354f3b1..0f06d68bc6 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -451,10 +451,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/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 71083a6257..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 @@ -199,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") { 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 a8842fbd80..87b0426aa3 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 @@ -51,6 +51,7 @@ mod transferable_streams; mod traversal; mod traversal_coordinator; 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]) + ); +}