Skip to content

Commit 0b84d33

Browse files
authored
TIKA-4812 - improve media file robustness
2 parents aaea543 + ada1ad1 commit 0b84d33

11 files changed

Lines changed: 547 additions & 110 deletions

File tree

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.InputStream;
2323
import java.io.PushbackInputStream;
2424
import java.io.UnsupportedEncodingException;
25+
import java.util.Arrays;
2526
import java.util.Iterator;
2627

2728
import org.apache.tika.parser.mp3.ID3Tags.ID3Comment;
@@ -180,9 +181,10 @@ protected static byte[] readFully(InputStream inp, int length, boolean shortData
180181
throw new IOException("Tried to read " + length + " bytes, but only " + pos +
181182
" bytes present");
182183
} else {
183-
// Give them what we found
184-
// TODO Log the short read
185-
return b;
184+
// truncated stream: return only the bytes actually read, not the
185+
// zero-padded full-length array, so callers (e.g. cover-art
186+
// extraction) don't emit padding as data. TIKA-4812
187+
return Arrays.copyOf(b, pos);
186188
}
187189
}
188190
pos += read;
@@ -668,8 +670,12 @@ protected RawTagIterator(int nameLength, int sizeLength, int sizeMultiplier,
668670
}
669671

670672
public boolean hasNext() {
671-
// Check for padding at the end
672-
return offset < data.length && data[offset] != 0;
673+
// Stop at padding, and at a truncated tail too short for a full frame
674+
// header: the RawTag constructor reads the header bytes unconditionally,
675+
// so without the data.length no longer being zero-padded (TIKA-4812) a
676+
// partial header would throw ArrayIndexOutOfBoundsException.
677+
return offset + nameLength + sizeLength + flagLength <= data.length
678+
&& data[offset] != 0;
673679
}
674680

675681
public RawTag next() {

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import java.util.Optional;
3232
import java.util.Set;
3333

34-
import com.drew.imaging.mp4.Mp4Reader;
3534
import com.drew.metadata.Directory;
3635
import com.drew.metadata.MetadataException;
3736
import com.drew.metadata.mp4.Mp4BoxHandler;
@@ -77,6 +76,14 @@ public class MP4Parser implements Parser {
7776
private static final MediaType AUDIO_MP4 = MediaType.audio("mp4");
7877

7978
private static final int MAX_ERROR_MESSAGES = 100;
79+
80+
//an accepted MP4 box whose declared payload exceeds this is skipped rather than
81+
//loaded, so a crafted box size cannot force a multi-GB allocation. Cover art and
82+
//other legitimate metadata boxes are well under this; configurable if a real file
83+
//needs more. See TikaMp4Reader and TIKA-4812.
84+
private static final long DEFAULT_MAX_BOX_SIZE = 100L * 1024L * 1024L;
85+
86+
private long maxBoxSize = DEFAULT_MAX_BOX_SIZE;
8087
static {
8188
// All types should be 4 bytes long, space padded as needed
8289
typesMap.put(MediaType.audio("mp4"), Arrays.asList("M4A ", "M4B ", "F4A ", "F4B "));
@@ -95,6 +102,22 @@ public Set<MediaType> getSupportedTypes(ParseContext context) {
95102
return SUPPORTED_TYPES;
96103
}
97104

105+
/**
106+
* The maximum declared payload, in bytes, of an accepted MP4 box that will be
107+
* read into memory; larger boxes are skipped. Guards against a crafted box size
108+
* forcing a multi-GB allocation.
109+
*/
110+
public long getMaxBoxSize() {
111+
return maxBoxSize;
112+
}
113+
114+
public void setMaxBoxSize(long maxBoxSize) {
115+
if (maxBoxSize <= 0) {
116+
throw new IllegalArgumentException("maxBoxSize must be positive: " + maxBoxSize);
117+
}
118+
this.maxBoxSize = maxBoxSize;
119+
}
120+
98121
public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata,
99122
ParseContext context) throws IOException, SAXException, TikaException {
100123

@@ -105,8 +128,11 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
105128
Mp4BoxHandler boxHandler = new TikaMp4BoxHandler(mp4Metadata, metadata, xhtml, context);
106129
//we used to spool to disk and then read from that with sannies parser.
107130
//we think that drewnoakes' parser streams the data so we don't need to spool
131+
//when the length is known (file-backed), pass it so a box that claims more than
132+
//the input holds is skipped rather than allocated
133+
long inputLength = tis.hasLength() ? tis.getLength() : -1;
108134
try {
109-
Mp4Reader.extract(tis, boxHandler);
135+
TikaMp4Reader.extract(tis, boxHandler, maxBoxSize, inputLength);
110136
} catch (RuntimeSAXException e) {
111137
throw (SAXException) e.getCause();
112138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.mp4;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
22+
import com.drew.imaging.mp4.Mp4Handler;
23+
import com.drew.lang.StreamReader;
24+
import com.drew.metadata.mp4.Mp4BoxHandler;
25+
import com.drew.metadata.mp4.Mp4Context;
26+
import com.drew.metadata.mp4.Mp4MediaHandler;
27+
28+
/**
29+
* A size-bounded reimplementation of com.drew.imaging.mp4.Mp4Reader.
30+
* <p>
31+
* The metadata-extractor reader eagerly does {@code new byte[(int) boxSize - 8]}
32+
* for every box a handler accepts, with {@code boxSize} attacker-controlled and
33+
* capped only at {@code Integer.MAX_VALUE} (~2GB), and {@code StreamReader.getBytes}
34+
* allocates before checking how much data is actually present. A single crafted
35+
* box header therefore forces a multi-GB allocation. This reader is identical to
36+
* the library's box walk except that an accepted box whose payload exceeds
37+
* {@code maxBoxSize} is skipped (a lazy stream advance, no allocation) instead of
38+
* being read. Boxes the handler does not accept were already skipped by the
39+
* library, so this only bounds the boxes we opt into. See TIKA-4812.
40+
*/
41+
final class TikaMp4Reader {
42+
43+
private TikaMp4Reader() {
44+
}
45+
46+
//MP4 containers nest (moov/trak/mdia/minf/stbl/udta/meta); cap the recursion so a
47+
//crafted chain of nested container headers cannot overflow the stack (an uncaught
48+
//Error, caught by neither the IOException handler below nor CompositeParser). Real
49+
//files nest well under this.
50+
private static final int MAX_BOX_DEPTH = 100;
51+
52+
/**
53+
* @param inputLength total input length in bytes, or -1 if unknown. When known, a box
54+
* that declares more payload than the input holds is skipped rather
55+
* than allocated (StreamReader.getBytes allocates before reading).
56+
*/
57+
static void extract(InputStream inputStream, Mp4BoxHandler handler, long maxBoxSize,
58+
long inputLength) {
59+
StreamReader reader = new StreamReader(inputStream);
60+
reader.setMotorolaByteOrder(true);
61+
processBoxes(reader, -1, handler, new Mp4Context(), maxBoxSize, inputLength, 0);
62+
}
63+
64+
private static void processBoxes(StreamReader reader, long atomEnd, Mp4Handler<?> handler,
65+
Mp4Context context, long maxBoxSize, long inputLength,
66+
int depth) {
67+
if (depth > MAX_BOX_DEPTH) {
68+
handler.addError("MP4 box nesting exceeds the maximum depth of " + MAX_BOX_DEPTH);
69+
return;
70+
}
71+
try {
72+
while (atomEnd == -1 || reader.getPosition() < atomEnd) {
73+
long boxSize = reader.getUInt32();
74+
String boxType = reader.getString(4);
75+
//4 bytes size + 4 bytes type, plus 8 more when a 64-bit largesize follows
76+
int headerSize = boxSize == 1 ? 16 : 8;
77+
if (headerSize == 16) {
78+
boxSize = reader.getInt64();
79+
}
80+
if (boxSize > Integer.MAX_VALUE) {
81+
handler.addError("Box size too large.");
82+
break;
83+
}
84+
if (boxSize < headerSize) {
85+
handler.addError("Box size too small.");
86+
break;
87+
}
88+
89+
long payloadLength = boxSize - headerSize;
90+
if (acceptContainer(handler, boxType)) {
91+
processBoxes(reader, reader.getPosition() + payloadLength,
92+
processBox(handler, boxType, null, boxSize, context), context,
93+
maxBoxSize, inputLength, depth + 1);
94+
} else if (acceptBox(handler, boxType)) {
95+
//StreamReader.getBytes allocates the whole payload up front, so skip
96+
//(a lazy stream advance) any box over the cap, or one that claims more
97+
//than the input holds, instead of allocating it. Skip-and-continue is
98+
//deliberate: unlike the TikaMemoryLimitException other parsers throw,
99+
//this keeps the remaining boxes' metadata; the skip is recorded as a
100+
//warning via the directory's error list.
101+
boolean tooLarge = payloadLength > maxBoxSize;
102+
boolean beyondInput = inputLength >= 0
103+
&& reader.getPosition() + payloadLength > inputLength;
104+
if (tooLarge || beyondInput) {
105+
handler.addError("MP4 box '" + boxType + "' payload (" + payloadLength
106+
+ " bytes) exceeds the "
107+
+ (tooLarge ? "maximum of " + maxBoxSize + " bytes" : "input size")
108+
+ "; skipping.");
109+
reader.skip(payloadLength);
110+
} else {
111+
handler = processBox(handler, boxType,
112+
reader.getBytes((int) payloadLength), boxSize, context);
113+
}
114+
} else {
115+
reader.skip(payloadLength);
116+
}
117+
}
118+
} catch (IOException e) {
119+
handler.addError(e.getMessage() == null ? "IOException reading MP4 boxes"
120+
: e.getMessage());
121+
}
122+
}
123+
124+
//the box walk holds handlers as Mp4Handler, whose accept/process methods are
125+
//protected; every concrete handler in play (Mp4BoxHandler-rooted, or an
126+
//Mp4MediaHandler track handler swapped in on 'hdlr') widens them to public,
127+
//so dispatch through whichever of the two families the instance belongs to.
128+
//A container's handler is obtained with processBox(type, null, ...), which is
129+
//exactly what the library's protected processContainer does.
130+
131+
private static boolean acceptContainer(Mp4Handler<?> handler, String type) {
132+
return handler instanceof Mp4BoxHandler
133+
? ((Mp4BoxHandler) handler).shouldAcceptContainer(type)
134+
: ((Mp4MediaHandler<?>) handler).shouldAcceptContainer(type);
135+
}
136+
137+
private static boolean acceptBox(Mp4Handler<?> handler, String type) {
138+
return handler instanceof Mp4BoxHandler
139+
? ((Mp4BoxHandler) handler).shouldAcceptBox(type)
140+
: ((Mp4MediaHandler<?>) handler).shouldAcceptBox(type);
141+
}
142+
143+
private static Mp4Handler<?> processBox(Mp4Handler<?> handler, String type, byte[] payload,
144+
long boxSize, Mp4Context context) throws IOException {
145+
return handler instanceof Mp4BoxHandler
146+
? ((Mp4BoxHandler) handler).processBox(type, payload, boxSize, context)
147+
: ((Mp4MediaHandler<?>) handler).processBox(type, payload, boxSize, context);
148+
}
149+
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4SoundHandler.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ private void extractFromSampleDescriptions(byte[] b) {
7979
//sample entry: 8 byte header, 6 reserved, 2 data ref index,
8080
//then version-dependent fixed sound fields before child boxes
8181
int version = EndianUtils.getUShortBE(b, pos + 16);
82-
int bitRate = findEsdsAverageBitRate(b, pos + soundEntrySize(version), end);
82+
int bitRate = findEsdsAverageBitRate(b, pos + soundEntrySize(version), end, 0);
8383
if (bitRate > 0) {
8484
tikaMetadata.set(Audio.BITRATE, bitRate);
8585
}
@@ -103,12 +103,21 @@ private static int soundEntrySize(int version) {
103103
return 36;
104104
}
105105

106+
//real files nest 'wave' at most one level; this only bounds crafted input,
107+
//where a deep chain of nested 'wave' boxes would otherwise recurse until the
108+
//stack overflows (an uncaught Error, not caught by Mp4Reader or CompositeParser).
109+
//See TIKA-4812.
110+
private static final int MAX_BOX_DEPTH = 10;
111+
106112
/**
107113
* Scans the child boxes of a sample entry for an 'esds' box and returns
108114
* its average bitrate, or 0 if there is none. QuickTime version 1/2
109115
* entries may nest the 'esds' inside a 'wave' extension box.
110116
*/
111-
private static int findEsdsAverageBitRate(byte[] b, int pos, int end) {
117+
private static int findEsdsAverageBitRate(byte[] b, int pos, int end, int depth) {
118+
if (depth > MAX_BOX_DEPTH) {
119+
return 0;
120+
}
112121
while (pos >= 0 && pos + 8 <= end) {
113122
long size = EndianUtils.getUIntBE(b, pos);
114123
if (size < 8 || size > end - pos) {
@@ -119,7 +128,7 @@ private static int findEsdsAverageBitRate(byte[] b, int pos, int end) {
119128
return readEsdsAverageBitRate(b, pos + 8, pos + (int) size);
120129
}
121130
if ("wave".equals(type)) {
122-
int nested = findEsdsAverageBitRate(b, pos + 8, pos + (int) size);
131+
int nested = findEsdsAverageBitRate(b, pos + 8, pos + (int) size, depth + 1);
123132
if (nested > 0) {
124133
return nested;
125134
}

0 commit comments

Comments
 (0)