From bf0e58e48bbbdb3f315f94d61216c9544acae379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Jirs=C3=A1k?= Date: Sun, 1 Jul 2018 11:27:15 +0200 Subject: [PATCH] #48 Validate QName inputs - throw IllegalArgumentException when qualified name contains disallowed character. --- src/java/org/dom4j/Namespace.java | 4 + src/java/org/dom4j/QName.java | 101 +++++++++++++++++++++++ src/java/org/dom4j/tree/QNameCache.java | 2 + src/test/org/dom4j/AllowedCharsTest.java | 78 +++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 src/test/org/dom4j/AllowedCharsTest.java diff --git a/src/java/org/dom4j/Namespace.java b/src/java/org/dom4j/Namespace.java index 7f78978d..0e04635c 100644 --- a/src/java/org/dom4j/Namespace.java +++ b/src/java/org/dom4j/Namespace.java @@ -51,6 +51,10 @@ public class Namespace extends AbstractNode { public Namespace(String prefix, String uri) { this.prefix = (prefix != null) ? prefix : ""; this.uri = (uri != null) ? uri : ""; + + if (!this.prefix.isEmpty()) { + QName.validateNCName(this.prefix); + } } /** diff --git a/src/java/org/dom4j/QName.java b/src/java/org/dom4j/QName.java index 955c0d38..de02e1d7 100644 --- a/src/java/org/dom4j/QName.java +++ b/src/java/org/dom4j/QName.java @@ -11,6 +11,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.util.regex.Pattern; import org.dom4j.tree.QNameCache; import org.dom4j.util.SingletonStrategy; @@ -23,11 +24,86 @@ *

* * @author James Strachan + * @author Filip Jirsák */ public class QName implements Serializable { /** The Singleton instance */ private static SingletonStrategy singleton = null; + /** + * {@code NameStartChar} without colon. + * + *
NameStartChar	::=	":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
+ * + * @see XML 1.0 – 2.3 Common Syntactic Constructs + * @see XML 1.1 – 2.3 Common Syntactic Constructs + */ + private static final String NAME_START_CHAR = "_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD"; + + /** + * {@code NameChar} without colon. + * + *
NameChar	::=	NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
+ * + * @see XML 1.0 – 2.3 Common Syntactic Constructs + * @see XML 1.1 – 2.3 Common Syntactic Constructs + */ + private static final String NAME_CHAR = NAME_START_CHAR + "-.0-9\u00B7\u0300-\u036F\u203F-\u2040"; + + /** + * {@code NCName} + * + *
+     * NCName		::=	NCNameStartChar NCNameChar*	(An XML Name, minus the ":")
+     * NCNameChar	::=	NameChar -':'
+     * NCNameStartChar	::=	NameStartChar -':'
+     * 
+ * + * @see Namespaces in XML 1.0 – 4 Qualified Names + * @see Namespaces in XML 1.1 – 4 Qualified Names + */ + private static final String NCNAME = "["+NAME_START_CHAR+"]["+NAME_CHAR+"]*"; + + /** + * Regular expression for {@code Name} (with colon). + * + *
Name	::=	NameStartChar (NameChar)*
+ * + * @see XML 1.0 – 2.3 Common Syntactic Constructs + * @see XML 1.1 – 2.3 Common Syntactic Constructs + */ + private static final Pattern RE_NAME = Pattern.compile("[:"+NAME_START_CHAR+"][:"+NAME_CHAR+"]*"); + + /** + * Regular expression for {@code NCName}. + * + *
+     * NCName		::=	NCNameStartChar NCNameChar*	(An XML Name, minus the ":")
+     * NCNameChar	::=	NameChar -':'
+     * NCNameStartChar	::=	NameStartChar -':'
+     * 
+ * + * @see Namespaces in XML 1.0 – 4 Qualified Names + * @see Namespaces in XML 1.1 – 4 Qualified Names + */ + private static final Pattern RE_NCNAME = Pattern.compile(NCNAME); + + /** + * Regular expression for {@code QName}. + * + *
+     * QName		::=	PrefixedName | UnprefixedName
+     * PrefixedName	::=	Prefix ':' LocalPart
+     * UnprefixedName	::=	LocalPart
+     * Prefix		::=	NCName
+     * LocalPart	::=	NCName
+     * 
+ * + * @see Namespaces in XML 1.0 – 4 Qualified Names + * @see Namespaces in XML 1.1 – 4 Qualified Names + */ + private static final Pattern RE_QNAME = Pattern.compile("(?:"+NCNAME+":)?"+NCNAME); + static { try { String defaultSingletonClass = "org.dom4j.util.SimpleSingleton"; @@ -73,6 +149,11 @@ public QName(String name, Namespace namespace) { this.name = (name == null) ? "" : name; this.namespace = (namespace == null) ? Namespace.NO_NAMESPACE : namespace; + if (this.namespace.equals(Namespace.NO_NAMESPACE)) { + validateName(this.name); + } else { + validateNCName(this.name); + } } public QName(String name, Namespace namespace, String qualifiedName) { @@ -80,6 +161,8 @@ public QName(String name, Namespace namespace, String qualifiedName) { this.qualifiedName = qualifiedName; this.namespace = (namespace == null) ? Namespace.NO_NAMESPACE : namespace; + validateNCName(this.name); + validateQName(this.qualifiedName); } public static QName get(String name) { @@ -253,6 +336,24 @@ private static QNameCache getCache() { QNameCache cache = (QNameCache) singleton.instance(); return cache; } + + private static void validateName(String name) { + if (!RE_NAME.matcher(name).matches()) { + throw new IllegalArgumentException(String.format("Illegal character in name: '%s'.", name)); + } + } + + protected static void validateNCName(String ncname) { + if (!RE_NCNAME.matcher(ncname).matches()) { + throw new IllegalArgumentException(String.format("Illegal character in local name: '%s'.", ncname)); + } + } + + private static void validateQName(String qname) { + if (!RE_QNAME.matcher(qname).matches()) { + throw new IllegalArgumentException(String.format("Illegal character in qualified name: '%s'.", qname)); + } + } } diff --git a/src/java/org/dom4j/tree/QNameCache.java b/src/java/org/dom4j/tree/QNameCache.java index 345a0934..3a518cd1 100644 --- a/src/java/org/dom4j/tree/QNameCache.java +++ b/src/java/org/dom4j/tree/QNameCache.java @@ -164,6 +164,8 @@ public QName get(String qualifiedName, String uri) { if (index < 0) { return get(qualifiedName, Namespace.get(uri)); + } else if (index == 0){ + throw new IllegalArgumentException("Qualified name cannot start with ':'."); } else { String name = qualifiedName.substring(index + 1); String prefix = qualifiedName.substring(0, index); diff --git a/src/test/org/dom4j/AllowedCharsTest.java b/src/test/org/dom4j/AllowedCharsTest.java new file mode 100644 index 00000000..20c1de0b --- /dev/null +++ b/src/test/org/dom4j/AllowedCharsTest.java @@ -0,0 +1,78 @@ +package org.dom4j; + +import org.testng.annotations.Test; + +/** + * @author Filip Jirsák + */ +public class AllowedCharsTest { + @Test + public void localName() { + QName.get("element"); + QName.get(":element"); + QName.get("elem:ent"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void localNameFail() { + QName.get("!element"); + } + + @Test + public void qname() { + QName.get("element", "http://example.com/namespace"); + QName.get("ns:element", "http://example.com/namespace"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void qnameFail1() { + QName.get("ns:elem:ent", "http://example.com/namespace"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void qnameFail2() { + QName.get(":nselement", "http://example.com/namespace"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void createElementLT() { + DocumentHelper.createElement("elementname"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void createElementAmpersand() { + DocumentHelper.createElement("element&name"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void addElement() { + Element root = DocumentHelper.createElement("root"); + root.addElement("element>name"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void addElementQualified() { + Element root = DocumentHelper.createElement("root"); + root.addElement("element>name", "http://example.com/namespace"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void addElementQualifiedPrefix() { + Element root = DocumentHelper.createElement("root"); + root.addElement("ns:element>name", "http://example.com/namespace"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void addElementPrefix() { + Element root = DocumentHelper.createElement("root"); + root.addElement("ns>:element", "http://example.com/namespace"); + } + + //TODO It is illegal to create element or attribute with namespace prefix and empty namespace IRI. + //See https://www.w3.org/TR/2006/REC-xml-names11-20060816/#scoping +}