This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch TIKA-4812-improve-media-file-robustness
in repository https://gitbox.apache.org/repos/asf/tika.git

commit 07bf18cf7d9b4b7be2f9a084b62b0db9d2eda19d
Author: tallison <[email protected]>
AuthorDate: Mon Aug 10 12:30:48 2026 -0400

    improve media parser robustness
---
 .../org/apache/tika/parser/mp3/ID3v2Frame.java     |  8 +++---
 .../org/apache/tika/parser/video/FLVParser.java    | 31 +++++++++++++++-------
 .../org/apache/tika/parser/mp3/ID3v2FrameTest.java | 13 +++++++++
 .../apache/tika/parser/video/FLVParserTest.java    | 22 +++++++++++++++
 4 files changed, 61 insertions(+), 13 deletions(-)

diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
index b8403deff0..2e03dab5a0 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.PushbackInputStream;
 import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
 import java.util.Iterator;
 
 import org.apache.tika.parser.mp3.ID3Tags.ID3Comment;
@@ -180,9 +181,10 @@ public class ID3v2Frame implements MP3Frame {
                     throw new IOException("Tried to read " + length + " bytes, 
but only " + pos +
                             " bytes present");
                 } else {
-                    // Give them what we found
-                    // TODO Log the short read
-                    return b;
+                    // truncated stream: return only the bytes actually read, 
not the
+                    // zero-padded full-length array, so callers (e.g. 
cover-art
+                    // extraction) don't emit padding as data. TIKA-4812
+                    return Arrays.copyOf(b, pos);
                 }
             }
             pos += read;
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/video/FLVParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/video/FLVParser.java
index 2feb07cad1..99d2377f6b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/video/FLVParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/video/FLVParser.java
@@ -96,7 +96,18 @@ public class FLVParser implements Parser {
         return uint;
     }
 
-    private Object readAMFData(DataInputStream input, int type) throws 
IOException {
+    //AMF objects/arrays nest recursively; cap the depth so a crafted metadata
+    //blob of deeply nested containers cannot overflow the stack (an uncaught 
Error)
+    private static final int MAX_AMF_DEPTH = 64;
+
+    Object readAMFData(DataInputStream input, int type) throws IOException {
+        return readAMFData(input, type, 0);
+    }
+
+    private Object readAMFData(DataInputStream input, int type, int depth) 
throws IOException {
+        if (depth > MAX_AMF_DEPTH) {
+            throw new IOException("AMF nesting exceeds the maximum depth of " 
+ MAX_AMF_DEPTH);
+        }
         if (type == -1) {
             type = input.readUnsignedByte();
         }
@@ -108,11 +119,11 @@ public class FLVParser implements Parser {
             case 2:
                 return readAMFString(input);
             case 3:
-                return readAMFObject(input);
+                return readAMFObject(input, depth);
             case 8:
-                return readAMFEcmaArray(input);
+                return readAMFEcmaArray(input, depth);
             case 10:
-                return readAMFStrictArray(input);
+                return readAMFStrictArray(input, depth);
             case 11:
                 final Date date = new Date((long) input.readDouble());
                 input.readShort(); // time zone
@@ -124,11 +135,11 @@ public class FLVParser implements Parser {
         }
     }
 
-    private Object readAMFStrictArray(DataInputStream input) throws 
IOException {
+    private Object readAMFStrictArray(DataInputStream input, int depth) throws 
IOException {
         long count = readUInt32(input);
         ArrayList<Object> list = new ArrayList<>();
         for (int i = 0; i < count; i++) {
-            list.add(readAMFData(input, -1));
+            list.add(readAMFData(input, -1, depth + 1));
         }
         return list;
     }
@@ -141,7 +152,7 @@ public class FLVParser implements Parser {
         return new String(chars, UTF_8);
     }
 
-    private Object readAMFObject(DataInputStream input) throws IOException {
+    private Object readAMFObject(DataInputStream input, int depth) throws 
IOException {
         HashMap<String, Object> array = new HashMap<>();
         while (true) {
             String key = readAMFString(input);
@@ -149,18 +160,18 @@ public class FLVParser implements Parser {
             if (dataType == 9) { // object end marker
                 break;
             }
-            array.put(key, readAMFData(input, dataType));
+            array.put(key, readAMFData(input, dataType, depth + 1));
         }
         return array;
     }
 
-    private Object readAMFEcmaArray(DataInputStream input) throws IOException {
+    private Object readAMFEcmaArray(DataInputStream input, int depth) throws 
IOException {
         long size = readUInt32(input);
         HashMap<String, Object> array = new HashMap<>();
         for (int i = 0; i < size; i++) {
             String key = readAMFString(input);
             int dataType = input.read();
-            array.put(key, readAMFData(input, dataType));
+            array.put(key, readAMFData(input, dataType, depth + 1));
         }
         return array;
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
index 8fee5d672c..b92f5a16c9 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java
@@ -20,9 +20,11 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_16BE;
 import static java.nio.charset.StandardCharsets.UTF_16LE;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -48,6 +50,17 @@ public class ID3v2FrameTest {
     private static final String LATIN = "Test Copyright";
     private static final String CJK = "日本語";
 
+    @Test
+    public void testReadFullyTruncatedReturnsActualBytes() throws IOException {
+        //a stream shorter than the declared length must yield only the bytes
+        //present, not a zero-padded full-length array, so a truncated frame's
+        //cover art is not emitted with trailing padding. TIKA-4812
+        byte[] present = "abcdefghij".getBytes(ISO_8859_1); //10 bytes
+        byte[] result = ID3v2Frame.readFully(new 
ByteArrayInputStream(present), 128, false);
+        assertArrayEquals(present, result);
+        assertEquals(present.length, result.length);
+    }
+
     private static byte[] frame(byte encodingFlag, byte[]... parts) {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         out.write(encodingFlag);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/video/FLVParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/video/FLVParserTest.java
index bd37dc3671..1c1a3bdb23 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/video/FLVParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/video/FLVParserTest.java
@@ -17,6 +17,12 @@
 package org.apache.tika.parser.video;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
 
 import org.junit.jupiter.api.Test;
 
@@ -25,6 +31,22 @@ import org.apache.tika.metadata.Metadata;
 
 public class FLVParserTest {
 
+    /**
+     * Deeply nested AMF objects used to recurse in readAMFData until the stack
+     * overflowed (an uncaught Error); the reader must bound the nesting depth.
+     */
+    @Test
+    public void testAmfNestingIsBounded() throws Exception {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(3); //top-level AMF object marker
+        for (int i = 0; i < 100_000; i++) {
+            bos.write(new byte[]{0, 0}); //empty key (uint16 length 0)
+            bos.write(3);                //value type = object -> recurse
+        }
+        DataInputStream dis = new DataInputStream(new 
ByteArrayInputStream(bos.toByteArray()));
+        assertThrows(IOException.class, () -> new FLVParser().readAMFData(dis, 
-1));
+    }
+
     @Test
     public void testFLV() throws Exception {
         String path = "/test-documents/testFLV.flv";

Reply via email to