This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new f7173fe3ca TIKA-4940: retire Tika's XML entity expansion limit (#3268)
f7173fe3ca is described below
commit f7173fe3caba99b0904b9124ce6304476d034e95
Author: Tim Allison <[email protected]>
AuthorDate: Mon Sep 28 15:44:48 2026 -0400
TIKA-4940: retire Tika's XML entity expansion limit (#3268)
---
.gitignore | 2 -
CHANGES.txt | 5 +-
.../java/org/apache/tika/utils/XMLReaderUtils.java | 274 ++-------------------
.../org/apache/tika/utils/XMLReaderUtilsTest.java | 33 +++
.../org/apache/tika/config/loader/TikaLoader.java | 5 +-
.../tika/serialization/config/GlobalSettings.java | 6 +-
6 files changed, 70 insertions(+), 255 deletions(-)
diff --git a/.gitignore b/.gitignore
index 00c9477de0..77ae19c8fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,6 @@ target
dependency-reduced-pom.xml
.idea
.classpath
-.checkstyle
.project
.settings
*.iml
@@ -27,5 +26,4 @@ tika-grpc/ignite
__pycache__/
*.pyc
-_remote.repositories
/.local_m2_repo/
diff --git a/CHANGES.txt b/CHANGES.txt
index d87c2a0cbf..4e6124d596 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,8 @@
-Release 4.1.1 - unreleased
+Release 4.2.0 - unreleased
+ * Retire Tika's entity expansion limit (default 20) in SAX, DOM and StAX
+ parsing in favor of standard Java configuration methods (TIKA-4940).
+
* Deprecate XMLReaderUtils.getXMLInputFactory() and use SAX for XFA
(TIKA-4938).
Release 4.1.0 - 9/26/2026
diff --git a/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java
b/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java
index 6ee4b4f26c..da1119f088 100644
--- a/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java
+++ b/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java
@@ -21,11 +21,9 @@ import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
-import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -75,6 +73,11 @@ public class XMLReaderUtils implements Serializable {
* and the pool of DOM builders
*/
public static final int DEFAULT_POOL_SIZE = 10;
+ /**
+ * @deprecated since 4.2.0, removal planned for 5.0; Tika no longer sets
an entity
+ * expansion limit. The JAXP provider's secure-processing limits apply.
+ */
+ @Deprecated
public static final int DEFAULT_MAX_ENTITY_EXPANSIONS = 20;
public static final int DEFAULT_NUM_REUSES = 100;
/**
@@ -82,11 +85,7 @@ public class XMLReaderUtils implements Serializable {
*/
private static final long serialVersionUID = 6110455808615143122L;
private static final Logger LOG =
LoggerFactory.getLogger(XMLReaderUtils.class);
- private static final String XERCES_SECURITY_MANAGER =
"org.apache.xerces.util.SecurityManager";
- private static final String XERCES_SECURITY_MANAGER_PROPERTY =
- "http://apache.org/xml/properties/security-manager";
-
- private static final AtomicBoolean HAS_WARNED_STAX = new
AtomicBoolean(false);
+ private static final AtomicBoolean HAS_WARNED_ENTITY_LIMIT = new
AtomicBoolean(false);
private static final ContentHandler IGNORING_CONTENT_HANDLER = new
DefaultHandler();
private static final DTDHandler IGNORING_DTD_HANDLER = new DTDHandler() {
@Override
@@ -117,7 +116,6 @@ public class XMLReaderUtils implements Serializable {
}
};
- private static final String JAXP_ENTITY_EXPANSION_LIMIT_KEY =
"jdk.xml.entityExpansionLimit";
//TODO: figure out if the rw lock is any better than a simple lock
//these lock the pool arrayblocking queues so that there isn't a race
condition
//of trying to acquire a parser while the pool is being resized
@@ -132,8 +130,6 @@ public class XMLReaderUtils implements Serializable {
*/
private static int POOL_SIZE = DEFAULT_POOL_SIZE;
private static int MAX_NUM_REUSES = DEFAULT_NUM_REUSES;
- private static long LAST_LOG = -1;
- private static volatile int MAX_ENTITY_EXPANSIONS =
determineMaxEntityExpansions();
private static ArrayBlockingQueue<PoolSAXParser> SAX_PARSERS =
new ArrayBlockingQueue<>(POOL_SIZE);
private static ArrayBlockingQueue<PoolDOMBuilder> DOM_BUILDERS =
@@ -147,21 +143,6 @@ public class XMLReaderUtils implements Serializable {
}
}
- private static int determineMaxEntityExpansions() {
- String expansionLimit =
System.getProperty(JAXP_ENTITY_EXPANSION_LIMIT_KEY);
- if (expansionLimit != null) {
- try {
- return Integer.parseInt(expansionLimit);
- } catch (NumberFormatException e) {
- LOG.warn(
- "Couldn't parse an integer for the entity expansion
limit: {}; " +
- "backing off to default: {}",
- expansionLimit, DEFAULT_MAX_ENTITY_EXPANSIONS);
- }
- }
- return DEFAULT_MAX_ENTITY_EXPANSIONS;
- }
-
/**
* Returns the XMLReader specified in this parsing context. If a reader
* is not explicitly specified, then one is created using the specified
@@ -199,9 +180,7 @@ public class XMLReaderUtils implements Serializable {
*/
public static SAXParser getSAXParser() throws TikaException {
try {
- SAXParser parser = getSAXParserFactory().newSAXParser();
- trySetXercesSecurityManager(parser);
- return parser;
+ return getSAXParserFactory().newSAXParser();
} catch (ParserConfigurationException e) {
throw new TikaException("Unable to configure a SAX parser", e);
} catch (SAXException e) {
@@ -247,8 +226,6 @@ public class XMLReaderUtils implements Serializable {
factory.setExpandEntityReferences(false);
factory.setValidating(false);
-
- trySetXercesSecurityManager(factory);
return factory;
}
@@ -293,8 +270,6 @@ public class XMLReaderUtils implements Serializable {
//try to cause DTDs to throw exceptions
tryToSetStaxProperty(factory, XMLInputFactory.SUPPORT_DTD, false);
-
- trySetStaxSecurityManager(factory);
return factory;
}
@@ -733,111 +708,6 @@ public class XMLReaderUtils implements Serializable {
}
}
- private static void trySetXercesSecurityManager(DocumentBuilderFactory
factory) {
- //from POI
- // Try built-in JVM one first, standalone if not
- for (String securityManagerClassName : new String[]{
- //"com.sun.org.apache.xerces.internal.util.SecurityManager",
- XERCES_SECURITY_MANAGER}) {
- try {
- Object mgr =
-
Class.forName(securityManagerClassName).getDeclaredConstructor().newInstance();
- Method setLimit =
mgr.getClass().getMethod("setEntityExpansionLimit",
- Integer.TYPE);
- setLimit.invoke(mgr, MAX_ENTITY_EXPANSIONS);
- factory.setAttribute(XERCES_SECURITY_MANAGER_PROPERTY, mgr);
- // Stop once one can be setup without error
- return;
- } catch (ClassNotFoundException e) {
- // continue without log, this is expected in some setups
- } catch (Throwable e) { // NOSONAR - also catch things like
NoClassDefError here
- // throttle the log somewhat as it can spam the log otherwise
- if (System.currentTimeMillis() > LAST_LOG +
TimeUnit.MINUTES.toMillis(5)) {
- LOG.warn(
- "SAX Security Manager could not be setup [log
suppressed for 5 " +
- "minutes]",
- e);
- LAST_LOG = System.currentTimeMillis();
- }
- }
- }
-
- // separate old version of Xerces not found => use the builtin way of
setting the property
- try {
-
factory.setAttribute("http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit",
- MAX_ENTITY_EXPANSIONS);
- } catch (IllegalArgumentException e) {
- // NOSONAR - also catch things like NoClassDefError here
- // throttle the log somewhat as it can spam the log otherwise
- if (System.currentTimeMillis() > LAST_LOG +
TimeUnit.MINUTES.toMillis(5)) {
- LOG.warn("SAX Security Manager could not be setup [log
suppressed for 5 minutes]",
- e);
- LAST_LOG = System.currentTimeMillis();
- }
- }
- }
-
- private static void trySetXercesSecurityManager(SAXParser parser) {
- //from POI
- // Try built-in JVM one first, standalone if not
- for (String securityManagerClassName : new String[]{
- //"com.sun.org.apache.xerces.internal.util.SecurityManager",
- XERCES_SECURITY_MANAGER}) {
- try {
- Object mgr =
-
Class.forName(securityManagerClassName).getDeclaredConstructor().newInstance();
- Method setLimit =
mgr.getClass().getMethod("setEntityExpansionLimit", Integer.TYPE);
- setLimit.invoke(mgr, MAX_ENTITY_EXPANSIONS);
-
- parser.setProperty(XERCES_SECURITY_MANAGER_PROPERTY, mgr);
- // Stop once one can be setup without error
- return;
- } catch (ClassNotFoundException e) {
- // continue without log, this is expected in some setups
- } catch (Throwable e) {
- // NOSONAR - also catch things like NoClassDefError here
- // throttle the log somewhat as it can spam the log otherwise
- if (System.currentTimeMillis() > LAST_LOG +
TimeUnit.MINUTES.toMillis(5)) {
- LOG.warn(
- "SAX Security Manager could not be setup [log
suppressed for 5 " +
- "minutes]",
- e);
- LAST_LOG = System.currentTimeMillis();
- }
- }
- }
-
- // separate old version of Xerces not found => use the builtin way of
setting the property
- try {
-
parser.setProperty("http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit",
- MAX_ENTITY_EXPANSIONS);
- } catch (SAXException e) { // NOSONAR - also catch things like
NoClassDefError here
- // throttle the log somewhat as it can spam the log otherwise
- if (System.currentTimeMillis() > LAST_LOG +
TimeUnit.MINUTES.toMillis(5)) {
- LOG.warn("SAX Security Manager could not be setup [log
suppressed for 5 minutes]",
- e);
- LAST_LOG = System.currentTimeMillis();
- }
- }
- }
-
- private static void trySetStaxSecurityManager(XMLInputFactory
inputFactory) {
- //try default java entity expansion, then fallback to woodstox, then
warn...once.
- try {
-
inputFactory.setProperty("http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit",
- MAX_ENTITY_EXPANSIONS);
- } catch (IllegalArgumentException e) {
- try {
- inputFactory.setProperty("com.ctc.wstx.maxEntityCount",
MAX_ENTITY_EXPANSIONS);
- } catch (IllegalArgumentException e2) {
- if (HAS_WARNED_STAX.getAndSet(true) == false) {
- LOG.warn("Could not set limit on maximum entity expansions
for: " + inputFactory.getClass());
- }
- }
-
- }
- }
-
/**
* Get the maximum number of times a SAXParser or DOMBuilder may be reused.
*
@@ -858,7 +728,7 @@ public class XMLReaderUtils implements Serializable {
/**
* Set the pool size for cached XML parsers. This has a side
* effect of locking the pool, and rebuilding the pool from
- * scratch with the most recent settings, such as {@link
#MAX_ENTITY_EXPANSIONS}
+ * scratch with the most recent settings.
*
* As of Tika 3.2.1, if a value of <code>0</code> is passed in, no
SAXParsers or DOMBuilders
* will be pooled, and a new parser/builder will be built for each parse.
@@ -917,26 +787,28 @@ public class XMLReaderUtils implements Serializable {
POOL_SIZE = poolSize;
}
+ /**
+ * @return -1: Tika sets no entity expansion limit; the JAXP provider's
+ * secure-processing limits apply
+ * @deprecated since 4.2.0, removal planned for 5.0
+ */
+ @Deprecated
public static int getMaxEntityExpansions() {
- return MAX_ENTITY_EXPANSIONS;
+ return -1;
}
/**
- * Set the maximum number of entity expansions allowable in SAX/DOM/StAX
parsing.
- * <b>NOTE:</b>A value less than or equal to zero indicates no limit.
- * This will override the system property {@link
#JAXP_ENTITY_EXPANSION_LIMIT_KEY}
- * and the {@link #DEFAULT_MAX_ENTITY_EXPANSIONS} value for allowable
entity expansions
- * <p>
- * <b>NOTE:</b> To trigger a rebuild of the pool of parsers with this
setting,
- * the client must call {@link #setPoolSize(int)} to rebuild the SAX and
DOM parsers
- * with this setting.
- * </p>
+ * No-op. Tika no longer sets an entity expansion limit; the JAXP
provider's
+ * secure-processing limits apply on every parse.
*
- * @param maxEntityExpansions -- maximum number of allowable entity
expansions
- * @since Apache Tika 1.19
+ * @deprecated since 4.2.0, removal planned for 5.0
*/
+ @Deprecated
public static void setMaxEntityExpansions(int maxEntityExpansions) {
- MAX_ENTITY_EXPANSIONS = maxEntityExpansions;
+ if (!HAS_WARNED_ENTITY_LIMIT.getAndSet(true)) {
+ LOG.warn("setMaxEntityExpansions is ignored since 4.2.0; " +
+ "the JAXP provider's secure-processing limits apply");
+ }
}
/**
@@ -954,66 +826,12 @@ public class XMLReaderUtils implements Serializable {
}
private static PoolSAXParser buildPoolParser(int generation, SAXParser
parser) {
- boolean canReset = false;
try {
parser.reset();
- canReset = true;
- } catch (UnsupportedOperationException e) {
- canReset = false;
- }
- boolean hasSecurityManager = false;
- try {
- Object mgr =
-
Class.forName(XERCES_SECURITY_MANAGER).getDeclaredConstructor().newInstance();
- Method setLimit =
mgr.getClass().getMethod("setEntityExpansionLimit", Integer.TYPE);
- setLimit.invoke(mgr, MAX_ENTITY_EXPANSIONS);
-
- parser.setProperty(XERCES_SECURITY_MANAGER_PROPERTY, mgr);
- hasSecurityManager = true;
- } catch (SecurityException e) {
- //don't swallow security exceptions
- throw e;
- } catch (ClassNotFoundException e) {
- // continue without log, this is expected in some setups
- } catch (Throwable e) {
- // NOSONAR - also catch things like NoClassDefError here
- // throttle the log somewhat as it can spam the log otherwise
- if (System.currentTimeMillis() > LAST_LOG +
TimeUnit.MINUTES.toMillis(5)) {
- LOG.warn("SAX Security Manager could not be setup [log
suppressed for 5 minutes]",
- e);
- LAST_LOG = System.currentTimeMillis();
- }
- }
-
- boolean canSetJaxPEntity = false;
- if (!hasSecurityManager) {
- // use the builtin way of setting the property
- try {
-
parser.setProperty("http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit",
- MAX_ENTITY_EXPANSIONS);
- canSetJaxPEntity = true;
- } catch (SAXException e) { // NOSONAR - also catch things like
NoClassDefError here
- // throttle the log somewhat as it can spam the log otherwise
- if (System.currentTimeMillis() > LAST_LOG +
TimeUnit.MINUTES.toMillis(5)) {
- LOG.warn(
- "SAX Security Manager could not be setup [log
suppressed for 5 " +
- "minutes]",
- e);
- LAST_LOG = System.currentTimeMillis();
- }
- }
- }
-
- if (!canReset && hasSecurityManager) {
- return new XercesPoolSAXParser(generation, parser);
- } else if (canReset && hasSecurityManager) {
- return new Xerces2PoolSAXParser(generation, parser);
- } else if (canReset && !hasSecurityManager && canSetJaxPEntity) {
return new BuiltInPoolSAXParser(generation, parser);
- } else {
+ } catch (UnsupportedOperationException e) {
return new UnrecognizedPoolSAXParser(generation, parser);
}
-
}
private static void clearReader(XMLReader reader) {
@@ -1080,46 +898,6 @@ public class XMLReaderUtils implements Serializable {
}
- private static class XercesPoolSAXParser extends PoolSAXParser {
- public XercesPoolSAXParser(int generation, SAXParser parser) {
- super(generation, parser);
- }
-
- @Override
- public void reset() {
- //don't do anything
- try {
- XMLReader reader = saxParser.getXMLReader();
- clearReader(reader);
- } catch (SAXException e) {
- //swallow
- }
- }
- }
-
- private static class Xerces2PoolSAXParser extends PoolSAXParser {
- public Xerces2PoolSAXParser(int generation, SAXParser parser) {
- super(generation, parser);
- }
-
- @Override
- void reset() {
- try {
- Object object =
saxParser.getProperty(XERCES_SECURITY_MANAGER_PROPERTY);
- saxParser.reset();
- saxParser.setProperty(XERCES_SECURITY_MANAGER_PROPERTY,
object);
- } catch (SAXException e) {
- LOG.warn("problem resetting sax parser", e);
- }
- try {
- XMLReader reader = saxParser.getXMLReader();
- clearReader(reader);
- } catch (SAXException e) {
- // ignored
- }
- }
- }
-
private static class BuiltInPoolSAXParser extends PoolSAXParser {
public BuiltInPoolSAXParser(int generation, SAXParser parser) {
super(generation, parser);
@@ -1137,9 +915,8 @@ public class XMLReaderUtils implements Serializable {
}
}
+ //parser that does not support reset(); try anyway on every release
private static class UnrecognizedPoolSAXParser extends PoolSAXParser {
- //if unrecognized, try to set all protections
- //and try to reset every time
public UnrecognizedPoolSAXParser(int generation, SAXParser parser) {
super(generation, parser);
}
@@ -1157,7 +934,6 @@ public class XMLReaderUtils implements Serializable {
} catch (SAXException e) {
// ignored
}
- trySetXercesSecurityManager(saxParser);
}
}
diff --git
a/tika-core/src/test/java/org/apache/tika/utils/XMLReaderUtilsTest.java
b/tika-core/src/test/java/org/apache/tika/utils/XMLReaderUtilsTest.java
index 3a70f0aefc..d7c30d8557 100644
--- a/tika-core/src/test/java/org/apache/tika/utils/XMLReaderUtilsTest.java
+++ b/tika-core/src/test/java/org/apache/tika/utils/XMLReaderUtilsTest.java
@@ -51,6 +51,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.ToTextContentHandler;
@@ -111,6 +112,9 @@ public class XMLReaderUtilsTest {
private static final String[] EXTERNAL_ENTITY_XMLS = new String[]{
EXTERNAL_DTD_SIMPLE_FILE, EXTERNAL_DTD_SIMPLE_URL,
EXTERNAL_ENTITY, EXTERNAL_LOCAL_DTD };
+ //above the entity expansion limit Tika set through 4.1.0
+ private static final int EXTERNAL_REFERENCES = 25;
+
private static final String[] BILLION_LAUGHS = new String[]{
BILLION_LAUGHS_CLASSICAL, BILLION_LAUGHS_VARIANT };
@AfterAll
@@ -390,6 +394,35 @@ public class XMLReaderUtilsTest {
assertEquals("beforeALLOWED_CONTENTafter", output.toString());
}
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testManyExternalReferencesParse(boolean dom, @TempDir Path
dir) throws Exception {
+ // external references resolve to nothing and must not fail the parse;
nothing is read
+ Path external = dir.resolve("external.dtd");
+ Files.writeString(external, "<!ENTITY ext 'LEAKED'>");
+ StringBuilder sb = new StringBuilder("<!DOCTYPE root SYSTEM '" +
external.toUri() +
+ "' [<!ENTITY ext SYSTEM '" + external.toUri() + "'>]><root>");
+ for (int i = 0; i < EXTERNAL_REFERENCES; i++) {
+ sb.append("<e>a&ext;b</e>");
+ }
+ String xml = sb.append("</root>").toString();
+ String text;
+ if (dom) {
+ Document doc = XMLReaderUtils.buildDOM(new StringReader(xml), new
ParseContext());
+ text = doc.getDocumentElement().getTextContent();
+ } else {
+ StringBuilder chars = new StringBuilder();
+ XMLReaderUtils.parseSAX(new StringReader(xml), new
DefaultHandler() {
+ @Override
+ public void characters(char[] ch, int start, int length) {
+ chars.append(ch, start, length);
+ }
+ }, new ParseContext());
+ text = chars.toString();
+ }
+ assertEquals("ab".repeat(EXTERNAL_REFERENCES), text);
+ }
+
private void limitCheck(SAXException e) throws SAXException {
String msg = e.getLocalizedMessage();
if (msg == null) {
diff --git
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
index 707a96ae5f..4e96125080 100644
---
a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
+++
b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
@@ -582,7 +582,6 @@ public class TikaLoader {
* "maxNumberLength": 500
* },
* "xml-reader-utils": {
- * "maxEntityExpansions": 1000,
* "maxNumReuses": 100,
* "poolSize": 10
* }
@@ -610,6 +609,10 @@ public class TikaLoader {
GlobalSettings.XmlReaderUtilsConfig xmlReaderUtilsConfig =
config.deserialize("xml-reader-utils",
GlobalSettings.XmlReaderUtilsConfig.class);
if (xmlReaderUtilsConfig != null) {
+ if (xmlReaderUtilsConfig.getMaxEntityExpansions() != null) {
+ LOG.warn("xml-reader-utils.maxEntityExpansions is ignored
since 4.2.0; " +
+ "the JAXP provider's secure-processing limits
apply");
+ }
globalSettings.setXmlReaderUtils(xmlReaderUtilsConfig);
}
}
diff --git
a/tika-serialization/src/main/java/org/apache/tika/serialization/config/GlobalSettings.java
b/tika-serialization/src/main/java/org/apache/tika/serialization/config/GlobalSettings.java
index 3100cf87e6..a7da03a923 100644
---
a/tika-serialization/src/main/java/org/apache/tika/serialization/config/GlobalSettings.java
+++
b/tika-serialization/src/main/java/org/apache/tika/serialization/config/GlobalSettings.java
@@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
* <pre>
* {
* "xml-reader-utils": {
- * "maxEntityExpansions": 1000,
* "maxNumReuses": 100,
* "poolSize": 10
* }
@@ -74,8 +73,11 @@ public class GlobalSettings {
*/
public static class XmlReaderUtilsConfig {
/**
- * Maximum entity expansions allowed in XML parsing.
+ * Ignored since 4.2.0: the JAXP provider's secure-processing limits
apply.
+ *
+ * @deprecated since 4.2.0, removal planned for 5.0
*/
+ @Deprecated
@JsonProperty("maxEntityExpansions")
private Integer maxEntityExpansions;