Skip to content

Commit 9656bde

Browse files
authored
Enable secure processing for the XML parser (#729)
Secure processing for XPath evaluation is already enabled but doesn't affect XML parsing.
1 parent 702afb1 commit 9656bde

File tree

6 files changed

+242
-55
lines changed

6 files changed

+242
-55
lines changed

src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.function.Function;
3131
import java.util.function.Supplier;
3232

33+
import javax.xml.parsers.DocumentBuilderFactory;
3334
import javax.xml.xpath.XPathFactory;
3435

3536
import org.apache.commons.text.StringSubstitutor;
@@ -1617,10 +1618,19 @@ public StringLookup xmlEncoderStringLookup() {
16171618
* if a lookup causes causes a path to resolve outside of these fences. Otherwise, the result is unfenced to preserved behavior from previous versions.
16181619
* </p>
16191620
* <p>
1620-
* We look up the value for the key in the format "DocumentPath:XPath".
1621+
* We looks up values in an XML document in the format {@code "[secure=(true|false):]DocumentPath:XPath"}.
16211622
* </p>
16221623
* <p>
1623-
* For example: "com/domain/document.xml:/path/to/node".
1624+
* For example:
1625+
* </p>
1626+
* <ul>
1627+
* <li>{@code "com/domain/document.xml:/path/to/node"}</li>
1628+
* <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
1629+
* <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
1630+
* </ul>
1631+
* <p>
1632+
* Secure processing is enabled by default. The secure boolean String parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The secure
1633+
* value in the key overrides instance settings given in the constructor.
16241634
* </p>
16251635
* <p>
16261636
* Using a {@link StringLookup} from the {@link StringLookupFactory}:
@@ -1644,7 +1654,7 @@ public StringLookup xmlEncoderStringLookup() {
16441654
* @since 1.5
16451655
*/
16461656
public StringLookup xmlStringLookup() {
1647-
return fences != null ? xmlStringLookup(XmlStringLookup.DEFAULT_FEATURES, fences) : XmlStringLookup.INSTANCE;
1657+
return fences != null ? xmlStringLookup(XmlStringLookup.DEFAULT_XPATH_FEATURES, fences) : XmlStringLookup.INSTANCE;
16481658
}
16491659

16501660
/**
@@ -1654,10 +1664,19 @@ public StringLookup xmlStringLookup() {
16541664
* if a lookup causes causes a path to resolve outside of these fences. Otherwise, the result is unfenced to preserved behavior from previous versions.
16551665
* </p>
16561666
* <p>
1657-
* We look up the value for the key in the format {@code "DocumentPath:XPath"}.
1667+
* We looks up values in an XML document in the format {@code "[secure=(true|false):]DocumentPath:XPath"}.
16581668
* </p>
16591669
* <p>
1660-
* For example: {@code "com/domain/document.xml:/path/to/node"}.
1670+
* For example:
1671+
* </p>
1672+
* <ul>
1673+
* <li>{@code "com/domain/document.xml:/path/to/node"}</li>
1674+
* <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
1675+
* <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
1676+
* </ul>
1677+
* <p>
1678+
* Secure processing is enabled by default. The secure boolean String parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The secure
1679+
* value in the key overrides instance settings given in the constructor.
16611680
* </p>
16621681
* <p>
16631682
* Using a {@link StringLookup} from the {@link StringLookupFactory}:
@@ -1677,13 +1696,14 @@ public StringLookup xmlStringLookup() {
16771696
* The examples above convert {@code "com/domain/document.xml:/path/to/node"} to the value of the XPath in the XML document.
16781697
* </p>
16791698
*
1680-
* @param xPathFactoryFeatures XPathFactory features to set.
1699+
* @param factoryFeatures DocumentBuilderFactory and XPathFactory features to set.
16811700
* @return An XML StringLookup instance.
1701+
* @see DocumentBuilderFactory#setFeature(String, boolean)
16821702
* @see XPathFactory#setFeature(String, boolean)
16831703
* @since 1.11.0
16841704
*/
1685-
public StringLookup xmlStringLookup(final Map<String, Boolean> xPathFactoryFeatures) {
1686-
return xmlStringLookup(xPathFactoryFeatures, fences);
1705+
public StringLookup xmlStringLookup(final Map<String, Boolean> factoryFeatures) {
1706+
return xmlStringLookup(factoryFeatures, fences);
16871707
}
16881708

16891709
/**
@@ -1693,10 +1713,19 @@ public StringLookup xmlStringLookup(final Map<String, Boolean> xPathFactoryFeatu
16931713
* if a lookup causes causes a path to resolve outside of these fences. Otherwise, the result is unfenced to preserved behavior from previous versions.
16941714
* </p>
16951715
* <p>
1696-
* We look up the value for the key in the format {@code "DocumentPath:XPath"}.
1716+
* We looks up values in an XML document in the format {@code "[secure=(true|false):]DocumentPath:XPath"}.
16971717
* </p>
16981718
* <p>
1699-
* For example: {@code "com/domain/document.xml:/path/to/node"}.
1719+
* For example:
1720+
* </p>
1721+
* <ul>
1722+
* <li>{@code "com/domain/document.xml:/path/to/node"}</li>
1723+
* <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
1724+
* <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
1725+
* </ul>
1726+
* <p>
1727+
* Secure processing is enabled by default. The secure boolean String parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The secure
1728+
* value in the key overrides instance settings given in the constructor.
17001729
* </p>
17011730
* <p>
17021731
* Using a {@link StringLookup} from the {@link StringLookupFactory} fenced by the current directory ({@code Paths.get("")}):
@@ -1711,10 +1740,8 @@ public StringLookup xmlStringLookup(final Map<String, Boolean> xPathFactoryFeatu
17111740
*
17121741
* <pre>
17131742
* StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("com/domain/document.xml:/path/to/node");
1714-
*
17151743
* // throws IllegalArgumentException
17161744
* StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("/rootdir/foo/document.xml:/path/to/node");
1717-
*
17181745
* // throws IllegalArgumentException
17191746
* StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("../com/domain/document.xml:/path/to/node");
17201747
* </pre>
@@ -1726,12 +1753,14 @@ public StringLookup xmlStringLookup(final Map<String, Boolean> xPathFactoryFeatu
17261753
* resolves in a fence.
17271754
* </p>
17281755
*
1729-
* @param xPathFactoryFeatures XPathFactory features to set.
1730-
* @param fences The fences guarding Path resolution.
1756+
* @param factoryFeatures DocumentBuilderFactory and XPathFactory features to set.
1757+
* @param fences The fences guarding Path resolution.
17311758
* @return An XML StringLookup instance.
1759+
* @see DocumentBuilderFactory#setFeature(String, boolean)
1760+
* @see XPathFactory#setFeature(String, boolean)
17321761
* @since 1.12.0
17331762
*/
1734-
public StringLookup xmlStringLookup(final Map<String, Boolean> xPathFactoryFeatures, final Path... fences) {
1735-
return new XmlStringLookup(xPathFactoryFeatures, fences);
1763+
public StringLookup xmlStringLookup(final Map<String, Boolean> factoryFeatures, final Path... fences) {
1764+
return new XmlStringLookup(factoryFeatures, factoryFeatures, fences);
17361765
}
17371766
}

src/main/java/org/apache/commons/text/lookup/XmlStringLookup.java

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,62 +26,101 @@
2626
import java.util.Objects;
2727

2828
import javax.xml.XMLConstants;
29+
import javax.xml.parsers.DocumentBuilderFactory;
2930
import javax.xml.xpath.XPathFactory;
3031

3132
import org.apache.commons.lang3.StringUtils;
32-
import org.xml.sax.InputSource;
33+
import org.w3c.dom.Document;
3334

3435
/**
35-
* Looks up keys from an XML document.
36+
* Looks up values in an XML document in the format {@code "[secure=(true|false):]DocumentPath:XPath"}.
3637
* <p>
37-
* Looks up the value for a given key in the format "Document:XPath".
38+
* For example:
3839
* </p>
40+
* <ul>
41+
* <li>{@code "com/domain/document.xml:/path/to/node"}</li>
42+
* <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
43+
* <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
44+
* </ul>
3945
* <p>
40-
* For example: "com/domain/document.xml:/path/to/node".
46+
* Secure processing is enabled by default. The secure boolean String parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The secure
47+
* value in the key overrides instance settings given in the constructor.
4148
* </p>
4249
*
4350
* @since 1.5
4451
*/
4552
final class XmlStringLookup extends AbstractPathFencedLookup {
4653

54+
/**
55+
* Minimum number of key parts.
56+
*/
57+
private static final int KEY_PARTS_MIN = 2;
58+
59+
/**
60+
* Minimum number of key parts.
61+
*/
62+
private static final int KEY_PARTS_MAX = 3;
63+
4764
/**
4865
* Defines default XPath factory features.
4966
*/
50-
static final Map<String, Boolean> DEFAULT_FEATURES;
67+
static final Map<String, Boolean> DEFAULT_XPATH_FEATURES;
5168

69+
/**
70+
* Defines default XML factory features.
71+
*/
72+
static final Map<String, Boolean> DEFAULT_XML_FEATURES;
5273
static {
53-
DEFAULT_FEATURES = new HashMap<>(1);
54-
DEFAULT_FEATURES.put(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
74+
DEFAULT_XPATH_FEATURES = new HashMap<>(1);
75+
DEFAULT_XPATH_FEATURES.put(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
76+
DEFAULT_XML_FEATURES = new HashMap<>(1);
77+
DEFAULT_XML_FEATURES.put(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
5578
}
56-
5779
/**
58-
* Defines the singleton for this class.
80+
* Defines the singleton for this class with secure processing enabled.
5981
*/
60-
static final XmlStringLookup INSTANCE = new XmlStringLookup(DEFAULT_FEATURES, (Path[]) null);
82+
static final XmlStringLookup INSTANCE = new XmlStringLookup(DEFAULT_XML_FEATURES, DEFAULT_XPATH_FEATURES, (Path[]) null);
6183

6284
/**
6385
* Defines XPath factory features.
6486
*/
6587
private final Map<String, Boolean> xPathFactoryFeatures;
6688

6789
/**
68-
* No need to build instances for now.
90+
* Defines XML factory features.
91+
*/
92+
private final Map<String, Boolean> xmlFactoryFeatures;
93+
94+
/**
95+
* Constructs a new instance.
6996
*
70-
* @param xPathFactoryFeatures XPathFactory features to set.
97+
* @param xmlFactoryFeatures The {@link DocumentBuilderFactory} features to set.
98+
* @param xPathFactoryFeatures The {@link XPathFactory} features to set.
99+
* @see DocumentBuilderFactory#setFeature(String, boolean)
71100
* @see XPathFactory#setFeature(String, boolean)
72101
*/
73-
XmlStringLookup(final Map<String, Boolean> xPathFactoryFeatures, final Path... fences) {
102+
XmlStringLookup(final Map<String, Boolean> xmlFactoryFeatures, final Map<String, Boolean> xPathFactoryFeatures, final Path... fences) {
74103
super(fences);
104+
this.xmlFactoryFeatures = Objects.requireNonNull(xmlFactoryFeatures, "xmlFactoryFeatures");
75105
this.xPathFactoryFeatures = Objects.requireNonNull(xPathFactoryFeatures, "xPathFfactoryFeatures");
76106
}
77107

78108
/**
79-
* Looks up the value for the key in the format "DocumentPath:XPath".
109+
* Looks up a value for the key in the format {@code "[secure=(true|false):]DocumentPath:XPath"}.
110+
* <p>
111+
* For example:
112+
* </p>
113+
* <ul>
114+
* <li>{@code "com/domain/document.xml:/path/to/node"}</li>
115+
* <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
116+
* <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
117+
* </ul>
80118
* <p>
81-
* For example: "com/domain/document.xml:/path/to/node".
119+
* Secure processing is enabled by default. The secure boolean String parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The secure
120+
* value in the key overrides instance settings given in the constructor.
82121
* </p>
83122
*
84-
* @param key the key to be looked up, may be null
123+
* @param key the key to be looked up, may be null.
85124
* @return The value associated with the key.
86125
*/
87126
@Override
@@ -91,22 +130,42 @@ public String lookup(final String key) {
91130
}
92131
final String[] keys = key.split(SPLIT_STR);
93132
final int keyLen = keys.length;
94-
if (keyLen != 2) {
95-
throw IllegalArgumentExceptions.format("Bad XML key format [%s]; expected format is DocumentPath:XPath.",
96-
key);
133+
if (keyLen != KEY_PARTS_MIN && keyLen != KEY_PARTS_MAX) {
134+
throw IllegalArgumentExceptions.format("Bad XML key format '%s'; the expected format is [secure=(true|false):]DocumentPath:XPath.", key);
97135
}
98-
final String documentPath = keys[0];
99-
final String xpath = StringUtils.substringAfter(key, SPLIT_CH);
100-
try (InputStream inputStream = Files.newInputStream(getPath(documentPath))) {
101-
final XPathFactory factory = XPathFactory.newInstance();
102-
for (final Entry<String, Boolean> p : xPathFactoryFeatures.entrySet()) {
103-
factory.setFeature(p.getKey(), p.getValue());
136+
final boolean isKeySecure = keyLen == KEY_PARTS_MAX;
137+
final Boolean secure = isKeySecure ? parseSecureKey(keys, key) : null;
138+
final String documentPath = isKeySecure ? keys[1] : keys[0];
139+
final String xpath = StringUtils.substringAfterLast(key, SPLIT_CH);
140+
final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
141+
try {
142+
for (final Entry<String, Boolean> p : xmlFactoryFeatures.entrySet()) {
143+
dbFactory.setFeature(p.getKey(), p.getValue());
144+
}
145+
if (secure != null) {
146+
dbFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, secure.booleanValue());
147+
}
148+
try (InputStream inputStream = Files.newInputStream(getPath(documentPath))) {
149+
final Document doc = dbFactory.newDocumentBuilder().parse(inputStream);
150+
final XPathFactory factory = XPathFactory.newInstance();
151+
for (final Entry<String, Boolean> p : xPathFactoryFeatures.entrySet()) {
152+
factory.setFeature(p.getKey(), p.getValue());
153+
}
154+
if (secure != null) {
155+
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, secure.booleanValue());
156+
}
157+
return factory.newXPath().evaluate(xpath, doc);
104158
}
105-
return factory.newXPath().evaluate(xpath, new InputSource(inputStream));
106159
} catch (final Exception e) {
107-
throw IllegalArgumentExceptions.format(e, "Error looking up XML document [%s] and XPath [%s].",
108-
documentPath, xpath);
160+
throw new IllegalArgumentException(e);
109161
}
110162
}
111163

164+
private Boolean parseSecureKey(final String[] args, final String key) {
165+
final String[] secParts = args[0].split("=");
166+
if (secParts.length != 2 && !Objects.equals(secParts[0], "secure")) {
167+
throw IllegalArgumentExceptions.format("Bad XML key format '%s'; the expected format is [secure=(true|false):]DocumentPath:XPath.", key);
168+
}
169+
return Boolean.valueOf(secParts[1]);
170+
}
112171
}

src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,17 @@ void testXmlStringLookup() {
282282
XmlStringLookupTest.assertLookup(stringLookupFactory.xmlStringLookup(features));
283283
XmlStringLookupTest.assertLookup(stringLookupFactory.xmlStringLookup(new HashMap<>()));
284284
}
285+
286+
@Test
287+
void testXmlStringLookupExternalEntityOff() {
288+
assertThrows(IllegalArgumentException.class,
289+
() -> StringLookupFactory.INSTANCE.xmlStringLookup().apply(XmlStringLookupTest.DOC_DIR + "document-entity-ref.xml:/document/content"));
290+
}
291+
292+
@Test
293+
void testXmlStringLookupExternalEntityOn() {
294+
final String key = XmlStringLookupTest.DOC_DIR + "document-entity-ref.xml:/document/content";
295+
assertEquals(XmlStringLookupTest.DATA, StringLookupFactory.INSTANCE.xmlStringLookup(XmlStringLookupTest.EMPTY_MAP).apply(key).trim());
296+
}
297+
285298
}

0 commit comments

Comments
 (0)