This is an automated email from the ASF dual-hosted git repository.
lewismc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nutch.git
The following commit(s) were added to refs/heads/master by this push:
new 773b4321c NUTCH-3206 XML parsers should not be vulnerable to XXE
attacks (#954)
773b4321c is described below
commit 773b4321ca25921085ca7bee78499a50b62f34ea
Author: Lewis John McGibbney <[email protected]>
AuthorDate: Sun Aug 16 13:46:11 2026 -0600
NUTCH-3206 XML parsers should not be vulnerable to XXE attacks (#954)
---
src/java/org/apache/nutch/exchange/Exchanges.java | 3 +-
.../org/apache/nutch/indexer/IndexWriters.java | 3 +-
.../org/apache/nutch/parse/ParsePluginsReader.java | 3 +-
.../apache/nutch/plugin/PluginManifestParser.java | 3 +-
src/java/org/apache/nutch/tools/DmozParser.java | 5 +-
src/java/org/apache/nutch/util/DomUtil.java | 4 +-
src/java/org/apache/nutch/util/XmlUtil.java | 135 +++++++++++++++++++++
.../org/creativecommons/nutch/CCParseFilter.java | 13 +-
.../org/apache/nutch/protocol/httpclient/Http.java | 4 +-
.../urlnormalizer/regex/RegexURLNormalizer.java | 5 +-
src/test/org/apache/nutch/util/TestXmlUtil.java | 90 ++++++++++++++
11 files changed, 251 insertions(+), 17 deletions(-)
diff --git a/src/java/org/apache/nutch/exchange/Exchanges.java
b/src/java/org/apache/nutch/exchange/Exchanges.java
index 1e0518b6b..37b2406bf 100644
--- a/src/java/org/apache/nutch/exchange/Exchanges.java
+++ b/src/java/org/apache/nutch/exchange/Exchanges.java
@@ -22,6 +22,7 @@ import org.apache.nutch.plugin.Extension;
import org.apache.nutch.plugin.ExtensionPoint;
import org.apache.nutch.plugin.PluginRepository;
import org.apache.nutch.plugin.PluginRuntimeException;
+import org.apache.nutch.util.XmlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
@@ -104,7 +105,7 @@ public class Exchanges {
final List<ExchangeConfig> configList = new LinkedList<>();
try {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilderFactory factory =
XmlUtil.newSecureDocumentBuilderFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
Element rootElement = builder.parse(inputSource).getDocumentElement();
NodeList exchangeList = rootElement.getElementsByTagName("exchange");
diff --git a/src/java/org/apache/nutch/indexer/IndexWriters.java
b/src/java/org/apache/nutch/indexer/IndexWriters.java
index f8ae8ee86..4b809d1e4 100644
--- a/src/java/org/apache/nutch/indexer/IndexWriters.java
+++ b/src/java/org/apache/nutch/indexer/IndexWriters.java
@@ -26,6 +26,7 @@ import org.apache.nutch.plugin.ExtensionPoint;
import org.apache.nutch.plugin.PluginRepository;
import org.apache.nutch.plugin.PluginRuntimeException;
import org.apache.nutch.util.NutchConfiguration;
+import org.apache.nutch.util.XmlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@@ -121,7 +122,7 @@ public class IndexWriters {
InputSource inputSource = new InputSource(ssInputStream);
try {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilderFactory factory =
XmlUtil.newSecureDocumentBuilderFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(inputSource);
Element rootElement = document.getDocumentElement();
diff --git a/src/java/org/apache/nutch/parse/ParsePluginsReader.java
b/src/java/org/apache/nutch/parse/ParsePluginsReader.java
index 978e3c097..8b84d4479 100644
--- a/src/java/org/apache/nutch/parse/ParsePluginsReader.java
+++ b/src/java/org/apache/nutch/parse/ParsePluginsReader.java
@@ -38,6 +38,7 @@ import org.slf4j.LoggerFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.nutch.util.NutchConfiguration;
+import org.apache.nutch.util.XmlUtil;
/**
* A reader to load the information stored in the
@@ -102,7 +103,7 @@ class ParsePluginsReader {
inputSource = new InputSource(ppInputStream);
try {
- factory = DocumentBuilderFactory.newInstance();
+ factory = XmlUtil.newSecureDocumentBuilderFactory();
parser = factory.newDocumentBuilder();
document = parser.parse(inputSource);
} catch (Exception e) {
diff --git a/src/java/org/apache/nutch/plugin/PluginManifestParser.java
b/src/java/org/apache/nutch/plugin/PluginManifestParser.java
index 95208fa43..dca202910 100644
--- a/src/java/org/apache/nutch/plugin/PluginManifestParser.java
+++ b/src/java/org/apache/nutch/plugin/PluginManifestParser.java
@@ -31,6 +31,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.hadoop.conf.Configuration;
+import org.apache.nutch.util.XmlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@@ -158,7 +159,7 @@ public class PluginManifestParser {
*/
private Document parseXML(URL url)
throws SAXException, IOException, ParserConfigurationException {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilderFactory factory = XmlUtil.newSecureDocumentBuilderFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(url.openStream());
}
diff --git a/src/java/org/apache/nutch/tools/DmozParser.java
b/src/java/org/apache/nutch/tools/DmozParser.java
index 4924e3b1b..5de457fb3 100644
--- a/src/java/org/apache/nutch/tools/DmozParser.java
+++ b/src/java/org/apache/nutch/tools/DmozParser.java
@@ -38,6 +38,7 @@ import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.MD5Hash;
import org.apache.nutch.util.NutchConfiguration;
+import org.apache.nutch.util.XmlUtil;
import org.apache.xerces.util.XMLChar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -296,9 +297,7 @@ public class DmozParser {
boolean includeAdult, int skew, Pattern topicPattern)
throws IOException, SAXException, ParserConfigurationException {
- SAXParserFactory parserFactory = SAXParserFactory.newInstance();
-
parserFactory.setFeature("http://xml.org/sax/features/external-general-entities",
false);
-
parserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",
true);
+ SAXParserFactory parserFactory = XmlUtil.newSecureSAXParserFactory();
SAXParser parser = parserFactory.newSAXParser();
XMLReader reader = parser.getXMLReader();
reader.setFeature("http://xml.org/sax/features/external-general-entities",
false);
diff --git a/src/java/org/apache/nutch/util/DomUtil.java
b/src/java/org/apache/nutch/util/DomUtil.java
index 50cc43c98..c31c2a1f0 100644
--- a/src/java/org/apache/nutch/util/DomUtil.java
+++ b/src/java/org/apache/nutch/util/DomUtil.java
@@ -60,6 +60,7 @@ public class DomUtil {
InputSource input;
try {
+ XmlUtil.configureSecure(parser);
input = new InputSource(is);
input.setEncoding("UTF-8");
parser.parse(input);
@@ -87,9 +88,10 @@ public class DomUtil {
public static void saveDom(OutputStream os, Element e) {
DOMSource source = new DOMSource(e);
- TransformerFactory transFactory = TransformerFactory.newInstance();
+ TransformerFactory transFactory;
Transformer transformer;
try {
+ transFactory = XmlUtil.newSecureTransformerFactory();
transformer = transFactory.newTransformer();
transformer.setOutputProperty("indent", "yes");
transformer.setOutputProperty(OutputKeys.ENCODING,
diff --git a/src/java/org/apache/nutch/util/XmlUtil.java
b/src/java/org/apache/nutch/util/XmlUtil.java
new file mode 100644
index 000000000..a8ce9b64f
--- /dev/null
+++ b/src/java/org/apache/nutch/util/XmlUtil.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nutch.util;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
+
+import org.apache.xerces.parsers.DOMParser;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+
+/**
+ * Helpers that return XML parser/transformer factories configured to reject
+ * XXE (external entity) attacks. Prefer these over bare
+ * {@link DocumentBuilderFactory#newInstance()} /
+ * {@link SAXParserFactory#newInstance()} /
+ * {@link TransformerFactory#newInstance()}.
+ */
+public final class XmlUtil {
+
+ private static final String DISALLOW_DOCTYPE_DECL =
+ "http://apache.org/xml/features/disallow-doctype-decl";
+ private static final String EXTERNAL_GENERAL_ENTITIES =
+ "http://xml.org/sax/features/external-general-entities";
+ private static final String EXTERNAL_PARAMETER_ENTITIES =
+ "http://xml.org/sax/features/external-parameter-entities";
+ private static final String LOAD_EXTERNAL_DTD =
+ "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+
+ private XmlUtil() {
+ }
+
+ /**
+ * @return a {@link DocumentBuilderFactory} hardened against XXE
+ * @throws ParserConfigurationException if a required secure feature is
+ * unsupported
+ */
+ public static DocumentBuilderFactory newSecureDocumentBuilderFactory()
+ throws ParserConfigurationException {
+ return newSecureDocumentBuilderFactory(false);
+ }
+
+ /**
+ * @param namespaceAware whether the factory should be namespace-aware
+ * @return a {@link DocumentBuilderFactory} hardened against XXE
+ * @throws ParserConfigurationException if a required secure feature is
+ * unsupported
+ */
+ public static DocumentBuilderFactory newSecureDocumentBuilderFactory(
+ boolean namespaceAware) throws ParserConfigurationException {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(namespaceAware);
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ factory.setFeature(DISALLOW_DOCTYPE_DECL, true);
+ factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
+ factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
+ factory.setFeature(LOAD_EXTERNAL_DTD, false);
+ factory.setXIncludeAware(false);
+ // Sonar java:S2755 "Going the extra mile" — does not alone block XXE
+ factory.setExpandEntityReferences(false);
+ return factory;
+ }
+
+ /**
+ * @return a {@link SAXParserFactory} hardened against XXE
+ * @throws ParserConfigurationException if a required secure feature is
+ * unsupported
+ * @throws SAXNotRecognizedException if a feature name is not recognized
+ * @throws SAXNotSupportedException if a feature value is not supported
+ */
+ public static SAXParserFactory newSecureSAXParserFactory()
+ throws ParserConfigurationException, SAXNotRecognizedException,
+ SAXNotSupportedException {
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ factory.setFeature(DISALLOW_DOCTYPE_DECL, true);
+ factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
+ factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
+ factory.setXIncludeAware(false);
+ return factory;
+ }
+
+ /**
+ * @return a {@link TransformerFactory} that cannot load external DTDs or
+ * stylesheets
+ * @throws TransformerConfigurationException if secure attributes cannot be
+ * applied
+ */
+ public static TransformerFactory newSecureTransformerFactory()
+ throws TransformerConfigurationException {
+ TransformerFactory factory = TransformerFactory.newInstance();
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+ factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
+ return factory;
+ }
+
+ /**
+ * Apply XXE-hardening features to an Xerces {@link DOMParser}.
+ *
+ * @param parser parser to configure
+ * @throws SAXException if a secure feature cannot be set
+ */
+ public static void configureSecure(DOMParser parser) throws SAXException {
+ parser.setFeature(DISALLOW_DOCTYPE_DECL, true);
+ parser.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
+ parser.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
+ parser.setFeature(LOAD_EXTERNAL_DTD, false);
+ try {
+ parser.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ } catch (SAXNotRecognizedException | SAXNotSupportedException e) {
+ // Xerces' DOMParser does not expose the JAXP secure-processing feature.
+ // DTDs and external entities are already disabled above.
+ }
+ }
+}
diff --git
a/src/plugin/creativecommons/src/java/org/creativecommons/nutch/CCParseFilter.java
b/src/plugin/creativecommons/src/java/org/creativecommons/nutch/CCParseFilter.java
index 030c6bf66..ee4e22ec2 100644
---
a/src/plugin/creativecommons/src/java/org/creativecommons/nutch/CCParseFilter.java
+++
b/src/plugin/creativecommons/src/java/org/creativecommons/nutch/CCParseFilter.java
@@ -26,6 +26,7 @@ import org.apache.nutch.parse.ParseException;
import org.apache.nutch.parse.ParseResult;
import org.apache.nutch.parse.ParseStatus;
import org.apache.nutch.parse.ParseText;
+import org.apache.nutch.util.XmlUtil;
import org.apache.hadoop.conf.Configuration;
import org.slf4j.Logger;
@@ -45,6 +46,7 @@ import java.util.HashMap;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.InputSource;
@@ -167,11 +169,14 @@ public class CCParseFilter implements HtmlParseFilter {
}
}
- /** Configure a namespace aware XML parser. */
- private static final DocumentBuilderFactory FACTORY =
DocumentBuilderFactory
- .newInstance();
+ /** Configure a namespace aware XML parser hardened against XXE. */
+ private static final DocumentBuilderFactory FACTORY;
static {
- FACTORY.setNamespaceAware(true);
+ try {
+ FACTORY = XmlUtil.newSecureDocumentBuilderFactory(true);
+ } catch (ParserConfigurationException e) {
+ throw new ExceptionInInitializerError(e);
+ }
}
/** Creative Commons' namespace URI. */
diff --git
a/src/plugin/protocol-httpclient/src/java/org/apache/nutch/protocol/httpclient/Http.java
b/src/plugin/protocol-httpclient/src/java/org/apache/nutch/protocol/httpclient/Http.java
index ebeb652dd..6d66b9b9d 100644
---
a/src/plugin/protocol-httpclient/src/java/org/apache/nutch/protocol/httpclient/Http.java
+++
b/src/plugin/protocol-httpclient/src/java/org/apache/nutch/protocol/httpclient/Http.java
@@ -26,7 +26,6 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
-import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
@@ -55,6 +54,7 @@ import org.apache.nutch.protocol.ProtocolException;
import org.apache.nutch.protocol.http.api.HttpBase;
import org.apache.hadoop.conf.Configuration;
import org.apache.nutch.util.NutchConfiguration;
+import org.apache.nutch.util.XmlUtil;
/**
* <p>
@@ -275,7 +275,7 @@ public class Http extends HttpBase {
InputStream is = conf.getConfResourceAsInputStream(authFile);
if (is != null) {
- Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ Document doc =
XmlUtil.newSecureDocumentBuilderFactory().newDocumentBuilder()
.parse(is);
Element rootElement = doc.getDocumentElement();
diff --git
a/src/plugin/urlnormalizer-regex/src/java/org/apache/nutch/net/urlnormalizer/regex/RegexURLNormalizer.java
b/src/plugin/urlnormalizer-regex/src/java/org/apache/nutch/net/urlnormalizer/regex/RegexURLNormalizer.java
index 2aeb722b2..2714e3e19 100644
---
a/src/plugin/urlnormalizer-regex/src/java/org/apache/nutch/net/urlnormalizer/regex/RegexURLNormalizer.java
+++
b/src/plugin/urlnormalizer-regex/src/java/org/apache/nutch/net/urlnormalizer/regex/RegexURLNormalizer.java
@@ -32,13 +32,12 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
-import javax.xml.parsers.DocumentBuilderFactory;
-
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.nutch.net.URLNormalizer;
import org.apache.nutch.net.URLNormalizers;
import org.apache.nutch.util.NutchConfiguration;
+import org.apache.nutch.util.XmlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@@ -224,7 +223,7 @@ public class RegexURLNormalizer extends Configured
implements URLNormalizer {
try {
// borrowed heavily from code in Configuration.java
- Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ Document doc =
XmlUtil.newSecureDocumentBuilderFactory().newDocumentBuilder()
.parse(new InputSource(reader));
Element root = doc.getDocumentElement();
if (!"regex-normalize".equals(root.getTagName())) {
diff --git a/src/test/org/apache/nutch/util/TestXmlUtil.java
b/src/test/org/apache/nutch/util/TestXmlUtil.java
new file mode 100644
index 000000000..d5c9d7dae
--- /dev/null
+++ b/src/test/org/apache/nutch/util/TestXmlUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nutch.util;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Unit tests for {@link XmlUtil} XXE hardening.
+ */
+public class TestXmlUtil {
+
+ @Test
+ public void testSecureFactoryParsesSimpleDocument() throws Exception {
+ DocumentBuilderFactory factory = XmlUtil.newSecureDocumentBuilderFactory();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document doc = builder.parse(new ByteArrayInputStream(
+ "<root attr=\"ok\"><child/></root>".getBytes(StandardCharsets.UTF_8)));
+ assertEquals("root", doc.getDocumentElement().getTagName());
+ assertEquals("ok", doc.getDocumentElement().getAttribute("attr"));
+ }
+
+ @Test
+ public void testSecureFactoryRejectsDoctype() throws Exception {
+ DocumentBuilderFactory factory = XmlUtil.newSecureDocumentBuilderFactory();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ String xxe = "<?xml version=\"1.0\"?>"
+ + "<!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>"
+ + "<foo>&xxe;</foo>";
+ assertThrows(SAXException.class, () -> builder
+ .parse(new
ByteArrayInputStream(xxe.getBytes(StandardCharsets.UTF_8))));
+ }
+
+ @Test
+ public void testDomUtilRejectsDoctype() {
+ String xxe = "<?xml version=\"1.0\"?>"
+ + "<!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>"
+ + "<foo>&xxe;</foo>";
+ Element element = DomUtil.getDom(
+ new ByteArrayInputStream(xxe.getBytes(StandardCharsets.UTF_8)));
+ // DomUtil swallows parse errors and returns null
+ assertEquals(null, element);
+ }
+
+ @Test
+ public void testDomUtilParsesDocumentWithLeadingComment() {
+ String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<!-- just a comment -->"
+ + "<subcollections><subcollection><id>nutch</id></subcollection>"
+ + "</subcollections>";
+ Element element = DomUtil.getDom(
+ new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
+ assertNotNull(element);
+ assertEquals("subcollections", element.getTagName());
+ assertEquals(1, element.getElementsByTagName("subcollection").getLength());
+ }
+
+ @Test
+ public void testNamespaceAwareFactory() throws Exception {
+ DocumentBuilderFactory factory = XmlUtil
+ .newSecureDocumentBuilderFactory(true);
+ assertNotNull(factory.newDocumentBuilder());
+ }
+}