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 4af1cb0e31 TIKA-4812: MP4Parser skips oversized boxes instead of
allocating them (backport) (#3501)
4af1cb0e31 is described below
commit 4af1cb0e31484519be8c2434f29a034a654bf889
Author: Tim Allison <[email protected]>
AuthorDate: Mon Nov 2 12:24:12 2026 -0500
TIKA-4812: MP4Parser skips oversized boxes instead of allocating them
(backport) (#3501)
---
CHANGES.txt | 5 +
.../java/org/apache/tika/parser/mp4/MP4Parser.java | 23 +++-
.../org/apache/tika/parser/mp4/TikaMp4Reader.java | 136 +++++++++++++++++++++
.../tika/parser/mp4/boxes/TikaUserDataBox.java | 8 ++
.../org/apache/tika/parser/mp4/MP4ParserTest.java | 74 +++++++++++
.../tika/parser/mp4/boxes/TikaUserDataBoxTest.java | 52 ++++++++
6 files changed, 296 insertions(+), 2 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index a1360a1b57..27a0e3eb73 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,10 @@
Release 3.4.0 - ???
+ * MP4Parser no longer allocates a box's declared size up front: a box larger
than
+ maxBoxSize (default 100 MB, configurable) or than the file is skipped and
recorded as
+ a warning, so a crafted box header cannot force a multi-GB allocation. A
malformed
+ udta sub-box length no longer throws IllegalArgumentException (TIKA-4812).
+
* The tika-app GUI's Formatted Text view no longer lets a document make Swing
construct classes or fetch URLs: only plain elements and attributes reach
it, and
remote images are not loaded (TIKA-4992).
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
index 0b7e30b2aa..f165e206b2 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
@@ -33,7 +33,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import com.drew.imaging.mp4.Mp4Reader;
import com.drew.metadata.Directory;
import com.drew.metadata.MetadataException;
import com.drew.metadata.mp4.Mp4BoxHandler;
@@ -43,6 +42,7 @@ import com.drew.metadata.mp4.media.Mp4VideoDirectory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
+import org.apache.tika.config.Field;
import org.apache.tika.exception.RuntimeSAXException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TemporaryResources;
@@ -76,6 +76,11 @@ public class MP4Parser implements Parser {
private static final MediaType AUDIO_MP4 = MediaType.audio("mp4");
private static final int MAX_ERROR_MESSAGES = 100;
+
+ //an accepted box with a larger payload is skipped, not loaded (TIKA-4812)
+ private static final long DEFAULT_MAX_BOX_SIZE = 100L * 1024L * 1024L;
+
+ private long maxBoxSize = DEFAULT_MAX_BOX_SIZE;
static {
// All types should be 4 bytes long, space padded as needed
typesMap.put(MediaType.audio("mp4"), Arrays.asList("M4A ", "M4B ",
"F4A ", "F4B "));
@@ -94,6 +99,19 @@ public class MP4Parser implements Parser {
return SUPPORTED_TYPES;
}
+ /**
+ * The maximum payload, in bytes, of an MP4 box that will be read into
memory;
+ * larger boxes are skipped.
+ */
+ public long getMaxBoxSize() {
+ return maxBoxSize;
+ }
+
+ @Field
+ public void setMaxBoxSize(long maxBoxSize) {
+ this.maxBoxSize = maxBoxSize;
+ }
+
public void parse(InputStream stream, ContentHandler handler, Metadata
metadata,
ParseContext context) throws IOException, SAXException,
TikaException {
@@ -107,7 +125,8 @@ public class MP4Parser implements Parser {
com.drew.metadata.Metadata mp4Metadata = new
com.drew.metadata.Metadata();
Mp4BoxHandler boxHandler = new TikaMp4BoxHandler(mp4Metadata,
metadata, xhtml);
try {
- Mp4Reader.extract(is, boxHandler);
+ TikaMp4Reader.extract(is, boxHandler, maxBoxSize,
+ Files.size(tstream.getPath()));
} catch (RuntimeSAXException e) {
throw (SAXException) e.getCause();
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4Reader.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4Reader.java
new file mode 100644
index 0000000000..3f45d941bb
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4Reader.java
@@ -0,0 +1,136 @@
+/*
+ * 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.tika.parser.mp4;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.drew.imaging.mp4.Mp4Handler;
+import com.drew.lang.StreamReader;
+import com.drew.metadata.mp4.Mp4BoxHandler;
+import com.drew.metadata.mp4.Mp4Context;
+import com.drew.metadata.mp4.Mp4MediaHandler;
+
+/**
+ * A size-bounded copy of metadata-extractor 2.22.0's
com.drew.imaging.mp4.Mp4Reader.
+ * <p>
+ * The library reader does {@code new byte[(int) boxSize - 8]} for every box a
handler
+ * accepts, with {@code boxSize} taken from the file and capped only at
+ * {@code Integer.MAX_VALUE}, and {@code StreamReader.getBytes} allocates
before checking
+ * how much data is present, so one crafted box header forces a multi-GB
allocation.
+ * This walk is the library's except that an accepted box whose payload exceeds
+ * {@code maxBoxSize}, or the input, is skipped instead of read. See TIKA-4812.
+ */
+final class TikaMp4Reader {
+
+ //same limit as the library's walk
+ private static final int MAX_BOX_DEPTH = 100;
+
+ private TikaMp4Reader() {
+ }
+
+ /**
+ * @param inputLength total input length in bytes, or -1 if unknown
+ */
+ static void extract(InputStream inputStream, Mp4BoxHandler handler, long
maxBoxSize,
+ long inputLength) {
+ StreamReader reader = new StreamReader(inputStream);
+ reader.setMotorolaByteOrder(true);
+ processBoxes(reader, -1, 0, handler, new Mp4Context(), maxBoxSize,
inputLength);
+ }
+
+ private static void processBoxes(StreamReader reader, long atomEnd, int
depth,
+ Mp4Handler<?> handler, Mp4Context context,
+ long maxBoxSize, long inputLength) {
+ try {
+ if (depth > MAX_BOX_DEPTH) {
+ handler.addError("Box nesting too deep.");
+ reader.skip(atomEnd - reader.getPosition());
+ return;
+ }
+ while (atomEnd == -1 || reader.getPosition() < atomEnd) {
+ long boxSize = reader.getUInt32();
+ String boxType = reader.getString(4);
+ //4 bytes size + 4 bytes type, plus 8 more when a 64-bit
largesize follows
+ int headerSize = boxSize == 1 ? 16 : 8;
+ if (headerSize == 16) {
+ boxSize = reader.getInt64();
+ }
+ if (boxSize > Integer.MAX_VALUE) {
+ handler.addError("Box size too large.");
+ break;
+ }
+ if (boxSize < headerSize) {
+ handler.addError("Box size too small.");
+ break;
+ }
+
+ long payloadLength = boxSize - headerSize;
+ if (acceptContainer(handler, boxType)) {
+ processBoxes(reader, reader.getPosition() + payloadLength,
depth + 1,
+ processBox(handler, boxType, null, boxSize,
context), context,
+ maxBoxSize, inputLength);
+ } else if (acceptBox(handler, boxType)) {
+ boolean tooLarge = payloadLength > maxBoxSize;
+ boolean beyondInput = inputLength >= 0
+ && reader.getPosition() + payloadLength >
inputLength;
+ if (tooLarge || beyondInput) {
+ handler.addError("MP4 box '" + boxType + "' payload ("
+ payloadLength
+ + " bytes) exceeds the "
+ + (tooLarge ? "maximum of " + maxBoxSize + "
bytes" : "input size")
+ + "; skipping.");
+ reader.skip(payloadLength);
+ } else {
+ handler = processBox(handler, boxType,
+ reader.getBytes((int) payloadLength), boxSize,
context);
+ }
+ } else {
+ reader.skip(payloadLength);
+ }
+ }
+ } catch (IOException e) {
+ handler.addError(e.getMessage());
+ }
+ }
+
+ //Mp4Handler's accept/process methods are protected; Mp4BoxHandler and the
Mp4MediaHandler
+ //track handlers swapped in on 'hdlr' widen them to public. Any other
handler type accepts
+ //nothing. processBox(type, null, ...) is what the library's
processContainer does.
+
+ private static boolean acceptContainer(Mp4Handler<?> handler, String type)
{
+ if (handler instanceof Mp4BoxHandler) {
+ return ((Mp4BoxHandler) handler).shouldAcceptContainer(type);
+ }
+ return handler instanceof Mp4MediaHandler
+ && ((Mp4MediaHandler<?>) handler).shouldAcceptContainer(type);
+ }
+
+ private static boolean acceptBox(Mp4Handler<?> handler, String type) {
+ if (handler instanceof Mp4BoxHandler) {
+ return ((Mp4BoxHandler) handler).shouldAcceptBox(type);
+ }
+ return handler instanceof Mp4MediaHandler
+ && ((Mp4MediaHandler<?>) handler).shouldAcceptBox(type);
+ }
+
+ private static Mp4Handler<?> processBox(Mp4Handler<?> handler, String
type, byte[] payload,
+ long boxSize, Mp4Context context)
throws IOException {
+ return handler instanceof Mp4BoxHandler
+ ? ((Mp4BoxHandler) handler).processBox(type, payload, boxSize,
context)
+ : ((Mp4MediaHandler<?>) handler).processBox(type, payload,
boxSize, context);
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
index 4fd264aed3..b8d3b72961 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
@@ -119,6 +119,10 @@ public class TikaUserDataBox {
//this handles "free" types...not sure if there are others?
//will throw IOException if no ilist is found
while (! subType.equals(ILST)) {
+ //len < 8 is a negative skip: IllegalArgumentException, which
escapes the reader (TIKA-4812)
+ if (len < 8L || len >= Integer.MAX_VALUE) {
+ return;
+ }
reader.skip(len - 8);
len = reader.getUInt32();
subType = reader.getString(4, StandardCharsets.ISO_8859_1);
@@ -156,6 +160,10 @@ public class TikaUserDataBox {
} else if ("cpil".equals(fieldName)) {
int compilationId = (int)reader.getByte();
metadata.set(XMPDM.COMPILATION, compilationId);
+ //totalRead counts toRead; skip the rest to stay aligned
+ if (toRead > 1) {
+ reader.skip(toRead - 1L);
+ }
} else if ("trkn".equals(fieldName)) {
if (toRead == 8) {
long numA = reader.getUInt32();
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
index f0e5b75300..e67d76c559 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
@@ -17,9 +17,15 @@
package org.apache.tika.parser.mp4;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -34,6 +40,7 @@ import com.drew.metadata.mp4.media.Mp4VideoDirectory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.xml.sax.ContentHandler;
+import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.TikaTest;
import org.apache.tika.io.TikaInputStream;
@@ -43,6 +50,7 @@ import org.apache.tika.metadata.XMP;
import org.apache.tika.metadata.XMPDM;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
/**
@@ -319,4 +327,70 @@ public class MP4ParserTest extends TikaTest {
assertEquals(-98.7654,
Double.parseDouble(metadata.get(TikaCoreProperties.LONGITUDE)), 0.00001);
assertEquals(10.5,
Double.parseDouble(metadata.get(TikaCoreProperties.ALTITUDE)), 0.00001);
}
+
+ @Test
+ public void testOversizedBoxIsSkippedNotAllocated() throws Exception {
+ //the library reader would do new byte[payload] for an accepted box.
TIKA-4812
+ byte[] ftyp = ftypBox(24);
+ assertNull(majorBrand(ftyp, 8L)); //cap below the 16-byte
payload: skipped
+ assertEquals("isom", majorBrand(ftyp, 1000L)); //above it: read
+ }
+
+ @Test
+ public void testBoxClaimingMoreThanTheInputIsSkipped() throws Exception {
+ //a 24-byte file whose ftyp claims ~1.5 GB, under the cap only because
the cap is raised
+ byte[] ftyp = ftypBox(1_500_000_000);
+ Mp4Directory dir = walk(ftyp, Long.MAX_VALUE);
+ assertTrue(dir.getErrors().toString().contains("exceeds the input
size"),
+ dir.getErrors().toString());
+ }
+
+ @Test
+ public void testNestedContainerRecursionIsBounded() throws Exception {
+ int depth = 100_000;
+ ByteBuffer buf = ByteBuffer.allocate(depth * 8);
+ for (int k = 0; k < depth; k++) {
+ buf.putInt(8 * (depth - k));
+ buf.put("moov".getBytes(StandardCharsets.ISO_8859_1));
+ }
+ assertDoesNotThrow(() -> walk(buf.array(), 1000L));
+ }
+
+ @Test
+ public void testLargeSizeBoxHeaderAccounting() throws Exception {
+ //a 64-bit largesize box has a 16-byte header; the ftyp after it must
still parse
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ bos.write(new byte[]{0, 0, 0, 1});
+ bos.write("udta".getBytes(StandardCharsets.ISO_8859_1));
+ bos.write(new byte[]{0, 0, 0, 0, 0, 0, 0, 24});
+ bos.write(new byte[]{0, 0, 0, 8});
+ bos.write("free".getBytes(StandardCharsets.ISO_8859_1));
+ bos.write(ftypBox(24));
+ assertEquals("isom", majorBrand(bos.toByteArray(), 1000L));
+ }
+
+ private static String majorBrand(byte[] boxes, long maxBoxSize) throws
Exception {
+ Mp4Directory dir = walk(boxes, maxBoxSize);
+ return dir == null ? null :
dir.getString(Mp4Directory.TAG_MAJOR_BRAND);
+ }
+
+ private static Mp4Directory walk(byte[] boxes, long maxBoxSize) {
+ com.drew.metadata.Metadata mp4Metadata = new
com.drew.metadata.Metadata();
+ Metadata tikaMetadata = new Metadata();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(new
DefaultHandler(), tikaMetadata);
+ TikaMp4BoxHandler handler = new TikaMp4BoxHandler(mp4Metadata,
tikaMetadata, xhtml);
+ TikaMp4Reader.extract(new ByteArrayInputStream(boxes), handler,
maxBoxSize, boxes.length);
+ return mp4Metadata.getFirstDirectoryOfType(Mp4Directory.class);
+ }
+
+ private static byte[] ftypBox(int declaredSize) {
+ ByteBuffer buf = ByteBuffer.allocate(24);
+ buf.putInt(declaredSize);
+ buf.put("ftyp".getBytes(StandardCharsets.ISO_8859_1));
+ buf.put("isom".getBytes(StandardCharsets.ISO_8859_1)); //major brand
+ buf.putInt(0);
+ buf.put("mp41".getBytes(StandardCharsets.ISO_8859_1));
+ buf.put("mp42".getBytes(StandardCharsets.ISO_8859_1));
+ return buf.array();
+ }
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBoxTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBoxTest.java
new file mode 100644
index 0000000000..1980d04fca
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBoxTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tika.parser.mp4.boxes;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+public class TikaUserDataBoxTest {
+
+ @Test
+ public void testIlstSearchMalformedSubBoxLength() {
+ //a sub-box length below 8 made skip(len - 8) negative: an
IllegalArgumentException
+ //that escaped the MP4 reader. TIKA-4812
+ ByteBuffer buf = ByteBuffer.allocate(40);
+ buf.putInt(40);
+ buf.put("meta".getBytes(StandardCharsets.ISO_8859_1));
+ buf.putInt(0);
+ buf.putInt(20); //lengthToStartOfList
= 16: no skip
+ buf.put("hdlr".getBytes(StandardCharsets.ISO_8859_1));
+ buf.putInt(0);
+ buf.putInt(0);
+ buf.put("mdir".getBytes(StandardCharsets.ISO_8859_1));
+ buf.putInt(4); //malformed sub-box
length
+ buf.put("free".getBytes(StandardCharsets.ISO_8859_1)); //not ilst:
enters the search
+
+ Metadata metadata = new Metadata();
+ XHTMLContentHandler xhtml = new XHTMLContentHandler(new
DefaultHandler(), metadata);
+ assertDoesNotThrow(() -> new TikaUserDataBox("udta", buf.array(),
metadata, xhtml));
+ }
+}