diff --git a/crates/js/src/xml.rs b/crates/js/src/xml.rs index 1277f5402..475870beb 100644 --- a/crates/js/src/xml.rs +++ b/crates/js/src/xml.rs @@ -27,6 +27,8 @@ fn parser_units(s: &str) -> Vec { pub(crate) const STAR: &[u16] = &[0x2A]; const XML_URI: &str = "http://www.w3.org/XML/1998/namespace"; +const XMLNS_URI: &str = "http://www.w3.org/2000/xmlns/"; +pub(crate) const RESERVED_NS_BINDING: &str = "a namespace declaration uses a reserved prefix or URI"; /// The hidden binding that holds a function's default XML namespace URI. const DEFAULT_NS_BINDING: &[u16] = &[0, 0x64, 0x78, 0x6E, 0x73]; @@ -221,6 +223,30 @@ fn prefix_in_scope<'a>(stack: &'a [XmlNs], prefix: &[u16]) -> Option<&'a XmlNs> stack.iter().rev().find(|ns| ns.prefix.as_deref() == Some(prefix)) } +/// Whether Namespaces in XML allows declaring `prefix` for `uri`: `xml` only with its fixed URI, +/// `xmlns` never, and neither fixed URI with another prefix or as the default namespace. +pub(crate) fn ns_binding_allowed(prefix: &[u16], uri: &[u16]) -> bool { + if units_are(prefix, "xmlns") { + return false; + } + let is_xml_uri = units_are(uri, XML_URI); + if units_are(prefix, "xml") { + return is_xml_uri; + } + !is_xml_uri && !units_are(uri, XMLNS_URI) +} + +/// Whether a node named `name` can be written as XML: no name is in the `xmlns` namespace, and +/// no attribute in no namespace is named `xmlns`. +pub(crate) fn xml_name_writable(name: &XmlName, attribute: bool) -> bool { + let uri = name.uri.as_deref().unwrap_or_default(); + !units_are(uri, XMLNS_URI) && !(attribute && uri.is_empty() && units_are(&name.local, "xmlns")) +} + +fn units_are(units: &[u16], text: &str) -> bool { + units.iter().copied().eq(text.encode_utf16()) +} + /// A prefix no entry of `stack` uses. fn fresh_prefix(stack: &[XmlNs]) -> Key { let mut n = 0u32; @@ -361,6 +387,12 @@ impl Interp { /// A new node. What `node` refers to must be reachable from the roots. pub(crate) fn new_xml_node(&mut self, node: XmlNode) -> JsResult { + if let Some(name) = &node.name + && matches!(node.kind, XmlKind::Element | XmlKind::Attribute) + && !xml_name_writable(name, node.kind == XmlKind::Attribute) + { + return Err(self.type_error(RESERVED_NS_BINDING)); + } let extra = XML_NODE_BYTES + node.name.as_ref().map_or(0, name_bytes) + string_bytes(node.value.len()); self.reserve_heap(OBJECT_BYTES + extra)?; self.heap.bytes = self.heap.bytes.saturating_add(extra); @@ -432,6 +464,9 @@ impl Interp { /// Sets the default XML namespace of the running function, or of the global code. pub(crate) fn xml_set_default_uri(&mut self, uri: Key) -> JsResult<()> { + if units_are(&uri, XMLNS_URI) { + return Err(self.type_error(RESERVED_NS_BINDING)); + } let env = self.frame()?.var_env; if let Some(Env::Decl { bindings, .. }) = self.heap.env_mut(env) { let value = Value::String(Str::from_rc(uri)); @@ -2151,6 +2186,9 @@ impl Interp { for (start, end, value) in &raw_attrs { let raw = src.get(*start..*end).unwrap_or_default(); if raw == key("xmlns").as_ref() { + if !ns_binding_allowed(&[], value) { + return Err(self.syntax(RESERVED_NS_BINDING)); + } if !seen_decl.insert(Vec::new()) { return Err(self.syntax("a namespace is declared twice")); } @@ -2161,6 +2199,9 @@ impl Interp { if value.is_empty() { return Err(self.syntax("a namespace prefix cannot be declared with an empty URI")); } + if !ns_binding_allowed(local, value) { + return Err(self.syntax(RESERVED_NS_BINDING)); + } if !seen_decl.insert(local.to_vec()) { return Err(self.syntax("a namespace is declared twice")); } diff --git a/crates/js/src/xml_builtins.rs b/crates/js/src/xml_builtins.rs index 0076dbe94..a5cac2912 100644 --- a/crates/js/src/xml_builtins.rs +++ b/crates/js/src/xml_builtins.rs @@ -10,7 +10,9 @@ use crate::heap::{ use crate::interp::{Abrupt, Interp, JsResult}; use crate::string_builtins::{new_array, set_element}; use crate::value::{Str, Value}; -use crate::xml::{Access, Content, NameRef, STAR, Xv, canonical_index, is_ncname, is_script_xml_name}; +use crate::xml::{ + Access, Content, NameRef, RESERVED_NS_BINDING, STAR, Xv, canonical_index, is_ncname, is_script_xml_name, ns_binding_allowed, xml_name_writable, +}; const SETTINGS: [(&str, Value); 5] = [ ("ignoreComments", Value::Bool(false)), @@ -807,6 +809,9 @@ fn add_in_scope(interp: &mut Interp, x: ObjId, ns: XmlNs) -> JsResult<()> { return Ok(()); } let Some(prefix) = ns.prefix.clone() else { return Ok(()) }; + if !ns_binding_allowed(&prefix, &ns.uri) { + return Err(interp.type_error(RESERVED_NS_BINDING)); + } let node = interp.xml_node(x)?; if prefix.is_empty() && node.name.as_ref().and_then(|n| n.uri.as_ref()).is_some_and(|u| u.is_empty()) { return Ok(()); @@ -998,7 +1003,8 @@ fn checked_local(interp: &mut Interp, local: &Key) -> JsResult<()> { fn set_local_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult { let x = this_node(interp, &this)?; - if matches!(interp.xml_kind(x)?, XmlKind::Text | XmlKind::Comment) { + let kind = interp.xml_kind(x)?; + if matches!(kind, XmlKind::Text | XmlKind::Comment) { return Ok(Value::Undefined); } let v = arg(args, 0); @@ -1007,12 +1013,26 @@ fn set_local_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> None => interp.to_str(&v)?.flat(), }; checked_local(interp, &local)?; + if kind == XmlKind::Attribute + && let Some(old) = interp.xml_node(x)?.name.clone() + { + check_name_binding(interp, &XmlName { local: local.clone(), ..old }, true)?; + } if let Some(n) = &mut interp.xml_node_mut(x)?.name { n.local = local; } Ok(Value::Undefined) } +/// Throws when an element or attribute named `name` could not be written as XML. +fn check_name_binding(interp: &mut Interp, name: &XmlName, attribute: bool) -> JsResult<()> { + let uri = name.uri.as_deref().unwrap_or_default(); + if !name.prefix.as_ref().is_none_or(|prefix| ns_binding_allowed(prefix, uri)) || !xml_name_writable(name, attribute) { + return Err(interp.type_error(RESERVED_NS_BINDING)); + } + Ok(()) +} + fn name_set_in_scope(interp: &mut Interp, x: ObjId, name: &XmlName) -> JsResult<()> { let ns = XmlNs { prefix: name.prefix.clone(), uri: name.uri.clone().unwrap_or_else(empty) }; match interp.xml_kind(x)? { @@ -1040,6 +1060,8 @@ fn set_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResu checked_local(interp, &name.local)?; if kind == XmlKind::Pi { name.uri = Some(empty()); + } else { + check_name_binding(interp, &name, kind == XmlKind::Attribute)?; } interp.xml_node_mut(x)?.name = Some(name.clone()); name_set_in_scope(interp, x, &name)?; @@ -1048,12 +1070,14 @@ fn set_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResu fn set_namespace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult { let x = this_node(interp, &this)?; - if matches!(interp.xml_kind(x)?, XmlKind::Text | XmlKind::Comment | XmlKind::Pi) { + let kind = interp.xml_kind(x)?; + if matches!(kind, XmlKind::Text | XmlKind::Comment | XmlKind::Pi) { return Ok(Value::Undefined); } let ns = build_namespace(interp, std::slice::from_ref(&arg(args, 0)))?; let Some(old) = interp.xml_node(x)?.name.clone() else { return Ok(Value::Undefined) }; let name = XmlName { uri: Some(ns.uri), local: old.local, prefix: ns.prefix }; + check_name_binding(interp, &name, kind == XmlKind::Attribute)?; interp.xml_node_mut(x)?.name = Some(name.clone()); name_set_in_scope(interp, x, &name)?; Ok(Value::Undefined) diff --git a/crates/js/src/xml_tests.rs b/crates/js/src/xml_tests.rs index 804234bef..f6cf4d0ef 100644 --- a/crates/js/src/xml_tests.rs +++ b/crates/js/src/xml_tests.rs @@ -360,6 +360,84 @@ fn namespaces_survive_serialization() { ); } +const XML_NS: &str = "http://www.w3.org/XML/1998/namespace"; +const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/"; + +#[test] +fn markup_cannot_declare_a_reserved_namespace_binding() { + let declarations = [ + "xmlns:xml='wrong'".to_string(), + "xmlns:xmlns='u'".to_string(), + format!("xmlns:xmlns='{XMLNS_NS}'"), + format!("xmlns:p='{XML_NS}'"), + format!("xmlns:p='{XMLNS_NS}'"), + format!("xmlns='{XML_NS}'"), + format!("xmlns='{XMLNS_NS}'"), + ]; + for declaration in declarations { + assert_eq!(thrown_name(&format!("")), "SyntaxError", "{declaration}"); + assert_eq!(thrown_name(&format!("new XML(\"\")")), "SyntaxError", "{declaration}"); + } + assert_eq!(string(&format!("().toXMLString()")), format!("")); + assert_eq!(string("().toXMLString()"), ""); +} + +#[test] +fn add_namespace_rejects_a_reserved_binding_and_keeps_the_element() { + for (prefix, uri) in [("xmlns", "u"), ("xmlns", XMLNS_NS), ("xml", "u"), ("p", XML_NS), ("p", XMLNS_NS)] { + let src = + format!("var x = ; var name = 'none'; try {{ x.addNamespace(new Namespace('{prefix}', '{uri}')); }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{prefix} {uri}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{prefix} {uri}"); + } + assert_eq!( + string(&format!("var x = ; x.addNamespace(new Namespace('xml', '{XML_NS}')); x.toXMLString()")), + format!("") + ); +} + +#[test] +fn names_cannot_write_a_reserved_namespace_binding() { + for setter in [ + "setNamespace(new Namespace('xmlns', 'u'))", + "setName(new QName(new Namespace('xml', 'u'), 'b'))", + "setNamespace(new Namespace('p', 'http://www.w3.org/2000/xmlns/'))", + "setNamespace('http://www.w3.org/2000/xmlns/')", + ] { + let src = format!("var x = ; var name = 'none'; try {{ x.{setter}; }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{setter}"); + } + let q = |prefix: &str| format!("new QName(new Namespace('{prefix}', 'u'), 'b')"); + assert_eq!(string(&format!("var x = ; x[{}] = '1'; x.toXMLString()", q("xmlns"))), "\n 1\n"); + assert_eq!(string(&format!("var x = ; x.@[{}] = '1'; x.toXMLString()", q("xml"))), ""); +} + +#[test] +fn an_attribute_cannot_be_renamed_xmlns() { + for setter in ["setName('xmlns')", "setLocalName('xmlns')"] { + let src = format!("var x = ; var name = 'none'; try {{ x.@b[0].{setter}; }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{setter}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{setter}"); + } +} + +#[test] +fn new_nodes_cannot_take_a_reserved_name() { + let q = format!("new QName('{XMLNS_NS}', 'b')"); + for update in ["x.@xmlns = 'u'".to_string(), format!("x[{q}] = '1'"), format!("x.@[{q}] = '1'"), format!("x[{q}][0] = '1'")] { + let src = format!("var x = ; var name = 'none'; try {{ {update}; }} catch (e) {{ name = e.name; }}"); + assert_eq!(string(&format!("{src} name")), "TypeError", "{update}"); + assert_eq!(string(&format!("{src} x.toXMLString()")), "", "{update}"); + } +} + +#[test] +fn the_xmlns_uri_cannot_be_the_default_namespace() { + assert_eq!(thrown_name(&format!("default xml namespace = '{XMLNS_NS}'")), "TypeError"); + assert_eq!(thrown_name(&format!("setDefaultXMLNamespace('{XMLNS_NS}')")), "TypeError"); +} + #[test] fn the_garbage_collector_keeps_trees_and_lists_alive() { let mut r = realm();