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,

Reply via email to