This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch branch_3x
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/branch_3x by this push:
new d529c1d0d7 improve depth limits (#3320)
d529c1d0d7 is described below
commit d529c1d0d788b1a497c9abaa306c8282fe58f596
Author: Tim Allison <[email protected]>
AuthorDate: Mon Oct 12 12:47:00 2026 -0400
improve depth limits (#3320)
---
.../apache/tika/parser/RecursiveParserWrapper.java | 54 ++++++++++++++++++++--
.../org/apache/tika/sax/SecureContentHandler.java | 17 ++++++-
.../tika/parser/RecursiveParserWrapperTest.java | 23 +++++++++
3 files changed, 89 insertions(+), 5 deletions(-)
diff --git
a/tika-core/src/main/java/org/apache/tika/parser/RecursiveParserWrapper.java
b/tika-core/src/main/java/org/apache/tika/parser/RecursiveParserWrapper.java
index 4e4f72dfa2..52b0025b08 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/RecursiveParserWrapper.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/RecursiveParserWrapper.java
@@ -159,10 +159,12 @@ public class RecursiveParserWrapper extends
ParserDecorator {
throwOnWriteLimitReached =
((WriteLimiter)factory).isThrowOnWriteLimitReached();
}
}
+ SecureHandlerCounter handlerCounter = null;
try {
TikaInputStream tis = TikaInputStream.get(stream, tmp, metadata);
+ handlerCounter = new SecureHandlerCounter(writeLimit, tis,
getAutoDetectParserConfig());
RecursivelySecureContentHandler secureContentHandler =
- new RecursivelySecureContentHandler(localHandler, tis, new
SecureHandlerCounter(writeLimit),
+ new RecursivelySecureContentHandler(localHandler, tis,
handlerCounter,
throwOnWriteLimitReached, context);
context.set(RecursivelySecureContentHandler.class,
secureContentHandler);
getWrappedParser().parse(tis, secureContentHandler, metadata,
context);
@@ -175,6 +177,9 @@ public class RecursiveParserWrapper extends ParserDecorator
{
} else {
String stackTrace = ExceptionUtils.getFilteredStackTrace(e);
metadata.add(TikaCoreProperties.CONTAINER_EXCEPTION,
stackTrace);
+ if (handlerCounter != null && handlerCounter.zipBombDetected) {
+ throw new TikaException("Zip bomb detected!", e);
+ }
throw e;
}
} finally {
@@ -187,6 +192,12 @@ public class RecursiveParserWrapper extends
ParserDecorator {
}
}
+ private AutoDetectParserConfig getAutoDetectParserConfig() {
+ Parser wrapped = getWrappedParser();
+ return wrapped instanceof AutoDetectParser ?
+ ((AutoDetectParser) wrapped).getAutoDetectParserConfig() :
null;
+ }
+
public static String getResourceName(Metadata metadata, AtomicInteger
counter) {
String objectName = "";
if (metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY) != null) {
@@ -277,6 +288,8 @@ public class RecursiveParserWrapper extends ParserDecorator
{
if (WriteLimitReachedException.isWriteLimitReached(e)) {
metadata.add(TikaCoreProperties.WRITE_LIMIT_REACHED,
"true");
throw e;
+ } else if (preParseHandler.handlerCounter.zipBombDetected) {
+ throw e;
} else {
if (catchEmbeddedExceptions) {
ParserUtils.recordParserFailure(this, e, metadata);
@@ -331,9 +344,18 @@ public class RecursiveParserWrapper extends
ParserDecorator {
private boolean writeLimitReached = false;
//total chars written to all handlers
private int totalChars = 0;
-
- private SecureHandlerCounter(int totalWriteLimit) {
+ //the container's stream: the denominator for the compression ratio
across the tree
+ private final TikaInputStream rootStream;
+ private final AutoDetectParserConfig config;
+ //total chars produced by all handlers, whether or not written
+ private long ratioChars = 0;
+ private boolean zipBombDetected = false;
+
+ private SecureHandlerCounter(int totalWriteLimit, TikaInputStream
rootStream,
+ AutoDetectParserConfig config) {
this.totalWriteLimit = totalWriteLimit;
+ this.rootStream = rootStream;
+ this.config = config;
}
/**
* Given the requested length, how many characters are actually
available
@@ -369,6 +391,32 @@ public class RecursiveParserWrapper extends
ParserDecorator {
this.handlerCounter = handlerCounter;
this.throwOnWriteLimitReached = throwOnWriteLimitReached;
this.parseContext = parseContext;
+ AutoDetectParserConfig config = handlerCounter.config;
+ if (config != null) {
+ if (config.getOutputThreshold() != null) {
+ setOutputThreshold(config.getOutputThreshold());
+ }
+ if (config.getMaximumCompressionRatio() != null) {
+
setMaximumCompressionRatio(config.getMaximumCompressionRatio());
+ }
+ }
+ }
+
+ /**
+ * Each embedded document's stream is decompressed, so the per-entry
ratio
+ * can't see archive compression. Also check the whole tree's output
+ * against the container's bytes, as the concatenated path does.
+ */
+ @Override
+ protected void advance(int length) throws SAXException {
+ super.advance(length);
+ handlerCounter.ratioChars += length;
+ try {
+ checkCompressionRatio(handlerCounter.ratioChars,
handlerCounter.rootStream);
+ } catch (SAXException e) {
+ handlerCounter.zipBombDetected = true;
+ throw e;
+ }
}
/**
diff --git
a/tika-core/src/main/java/org/apache/tika/sax/SecureContentHandler.java
b/tika-core/src/main/java/org/apache/tika/sax/SecureContentHandler.java
index fa9d682e89..015e8467f3 100644
--- a/tika-core/src/main/java/org/apache/tika/sax/SecureContentHandler.java
+++ b/tika-core/src/main/java/org/apache/tika/sax/SecureContentHandler.java
@@ -186,7 +186,7 @@ public class SecureContentHandler extends
ContentHandlerDecorator {
}
}
- private long getByteCount() throws SAXException {
+ private static long getByteCount(TikaInputStream stream) throws
SAXException {
try {
if (stream.hasLength()) {
return stream.getLength();
@@ -208,8 +208,21 @@ public class SecureContentHandler extends
ContentHandlerDecorator {
*/
protected void advance(int length) throws SAXException {
characterCount += length;
+ checkCompressionRatio(characterCount, stream);
+ }
+
+ /**
+ * Throws if {@code characterCount} exceeds the output threshold and the
maximum
+ * compression ratio against the bytes of {@code source}.
+ *
+ * @param characterCount output characters to check
+ * @param source input stream whose bytes produced them
+ * @throws SAXException if a zip bomb is detected
+ */
+ protected void checkCompressionRatio(long characterCount, TikaInputStream
source)
+ throws SAXException {
if (characterCount > threshold) {
- long byteCount = getByteCount();
+ long byteCount = getByteCount(source);
if (characterCount > byteCount * ratio) {
throw new SecureSAXException("Suspected zip bomb: " +
byteCount + " input bytes produced " + characterCount + " output characters");
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/RecursiveParserWrapperTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/RecursiveParserWrapperTest.java
index 4261db2990..34272fb926 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/RecursiveParserWrapperTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/RecursiveParserWrapperTest.java
@@ -19,16 +19,20 @@ package org.apache.tika.parser;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.zip.GZIPOutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ClosedInputStream;
@@ -513,6 +517,25 @@ public class RecursiveParserWrapperTest extends TikaTest {
}
}
+ @Test
+ public void testCompressionRatioAcrossEmbedded() throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (GZIPOutputStream gz = new GZIPOutputStream(bos)) {
+ byte[] line = ("a ".repeat(50) +
"\n").getBytes(StandardCharsets.US_ASCII);
+ for (int i = 0; i < 20000; i++) {
+ gz.write(line);
+ }
+ }
+ RecursiveParserWrapper wrapper = new
RecursiveParserWrapper(AUTO_DETECT_PARSER);
+ RecursiveParserWrapperHandler handler = new
RecursiveParserWrapperHandler(
+ new
BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1));
+ try (TikaInputStream tis = TikaInputStream.get(bos.toByteArray())) {
+ TikaException e = assertThrows(TikaException.class,
+ () -> wrapper.parse(tis, handler, new Metadata(), new
ParseContext()));
+ assertTrue(e.getMessage().contains("Zip bomb"));
+ }
+ }
+
private List<Metadata> getMetadata(Metadata metadata,
ContentHandlerFactory
contentHandlerFactory,
boolean catchEmbeddedExceptions,