Skip to content

Commit 8461301

Browse files
committed
TIKA-4812: address review feedback on media robustness
1 parent 07bf18c commit 8461301

9 files changed

Lines changed: 191 additions & 129 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: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,12 @@ protected RawTagIterator(int nameLength, int sizeLength, int sizeMultiplier,
670670
}
671671

672672
public boolean hasNext() {
673-
// Check for padding at the end
674-
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;
675679
}
676680

677681
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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
125125
Mp4BoxHandler boxHandler = new TikaMp4BoxHandler(mp4Metadata, metadata, xhtml, context);
126126
//we used to spool to disk and then read from that with sannies parser.
127127
//we think that drewnoakes' parser streams the data so we don't need to spool
128+
//when the length is known (file-backed), pass it so a box that claims more than
129+
//the input holds is skipped rather than allocated
130+
long inputLength = tis.hasLength() ? tis.getLength() : -1;
128131
try {
129-
TikaMp4Reader.extract(tis, boxHandler, maxBoxSize);
132+
TikaMp4Reader.extract(tis, boxHandler, maxBoxSize, inputLength);
130133
} catch (RuntimeSAXException e) {
131134
throw (SAXException) e.getCause();
132135
}

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

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,53 +43,76 @@ final class TikaMp4Reader {
4343
private TikaMp4Reader() {
4444
}
4545

46-
static void extract(InputStream inputStream, Mp4BoxHandler handler, long maxBoxSize) {
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) {
4759
StreamReader reader = new StreamReader(inputStream);
4860
reader.setMotorolaByteOrder(true);
49-
processBoxes(reader, -1, handler, new Mp4Context(), maxBoxSize);
61+
processBoxes(reader, -1, handler, new Mp4Context(), maxBoxSize, inputLength, 0);
5062
}
5163

5264
private static void processBoxes(StreamReader reader, long atomEnd, Mp4Handler<?> handler,
53-
Mp4Context context, long maxBoxSize) {
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+
}
5471
try {
5572
while (atomEnd == -1 || reader.getPosition() < atomEnd) {
5673
long boxSize = reader.getUInt32();
5774
String boxType = reader.getString(4);
58-
boolean isLargeSize = boxSize == 1;
59-
if (isLargeSize) {
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) {
6078
boxSize = reader.getInt64();
6179
}
6280
if (boxSize > Integer.MAX_VALUE) {
6381
handler.addError("Box size too large.");
6482
break;
6583
}
66-
if (boxSize < 8) {
84+
if (boxSize < headerSize) {
6785
handler.addError("Box size too small.");
6886
break;
6987
}
7088

89+
long payloadLength = boxSize - headerSize;
7190
if (acceptContainer(handler, boxType)) {
72-
processBoxes(reader, boxSize + reader.getPosition() - 8,
91+
processBoxes(reader, reader.getPosition() + payloadLength,
7392
processBox(handler, boxType, null, boxSize, context), context,
74-
maxBoxSize);
93+
maxBoxSize, inputLength, depth + 1);
7594
} else if (acceptBox(handler, boxType)) {
76-
long payloadLength = boxSize - 8;
77-
if (payloadLength > maxBoxSize) {
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) {
78105
handler.addError("MP4 box '" + boxType + "' payload (" + payloadLength
79-
+ " bytes) exceeds the maximum of " + maxBoxSize
80-
+ " bytes; skipping.");
106+
+ " bytes) exceeds the "
107+
+ (tooLarge ? "maximum of " + maxBoxSize + " bytes" : "input size")
108+
+ "; skipping.");
81109
reader.skip(payloadLength);
82110
} else {
83111
handler = processBox(handler, boxType,
84112
reader.getBytes((int) payloadLength), boxSize, context);
85113
}
86-
} else if (isLargeSize) {
87-
if (boxSize < 16) {
88-
break;
89-
}
90-
reader.skip(boxSize - 16);
91114
} else {
92-
reader.skip(boxSize - 8);
115+
reader.skip(payloadLength);
93116
}
94117
}
95118
} catch (IOException e) {

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: 1 addition & 5 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
}
@@ -114,10 +114,6 @@ private static int soundEntrySize(int version) {
114114
* its average bitrate, or 0 if there is none. QuickTime version 1/2
115115
* entries may nest the 'esds' inside a 'wave' extension box.
116116
*/
117-
private static int findEsdsAverageBitRate(byte[] b, int pos, int end) {
118-
return findEsdsAverageBitRate(b, pos, end, 0);
119-
}
120-
121117
private static int findEsdsAverageBitRate(byte[] b, int pos, int end, int depth) {
122118
if (depth > MAX_BOX_DEPTH) {
123119
return 0;

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

Lines changed: 73 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -115,28 +115,25 @@ private void parseUserDataBox(SequentialReader reader, String handlerType,
115115
int toSkip = lengthToStartOfList - read;
116116
reader.skip(toSkip);
117117
long len = reader.getUInt32();
118-
if (len >= Integer.MAX_VALUE || len <= 0) {
119-
//log
120-
return;
121-
}
122118
String subType = reader.getString(4, StandardCharsets.ISO_8859_1);
123-
//this handles "free" types...not sure if there are others?
124-
//will throw IOException if no ilist is found
125-
while (! subType.equals(ILST)) {
126-
//re-validate each re-read length: len < 8 makes skip(len - 8) negative,
127-
//which throws IllegalArgumentException (not IOException, so it escapes
128-
//MP4Reader). See TIKA-4812.
119+
//walk the "free"-style sub-boxes to the ilst, validating each declared length
120+
//once before it is used: a length below the 8-byte header would make skip(len - 8)
121+
//negative (an IllegalArgumentException that escapes MP4Reader), and an oversize one
122+
//would desync the walk. Raise a caught IOException instead, so the udta walk aborts
123+
//and the problem is recorded as a parse error rather than silently mis-read. It
124+
//also throws (EOFException) if no ilst is found. See TIKA-4812.
125+
while (! ILST.equals(subType)) {
129126
if (len < 8L || len >= Integer.MAX_VALUE) {
130-
return;
127+
throw new IOException("Malformed box length in udta metadata: " + len);
131128
}
132129
reader.skip(len - 8);
133130
len = reader.getUInt32();
134131
subType = reader.getString(4, StandardCharsets.ISO_8859_1);
135132
}
136-
if (ILST.equals(subType)) {
137-
processIList(reader, len);
133+
if (len < 8L || len >= Integer.MAX_VALUE) {
134+
throw new IOException("Malformed ilst length in udta metadata: " + len);
138135
}
139-
136+
processIList(reader, len);
140137
}
141138

142139

@@ -146,104 +143,83 @@ private void processIList(SequentialReader reader, long totalLen)
146143

147144
long totalRead = 0;
148145
while (totalRead < totalLen) {
146+
long recordStart = reader.getPosition();
149147
long recordLen = reader.getUInt32();
148+
if (recordLen < 16) {
149+
//malformed record header; stop rather than loop or skip a negative span
150+
return;
151+
}
150152
String fieldName = reader.getString(4, StandardCharsets.ISO_8859_1);
151153
long fieldLen = reader.getUInt32();
152154
String typeName = reader.getString(4, StandardCharsets.ISO_8859_1);//data
153-
totalRead += 16;
155+
long recordEnd = recordStart + recordLen;
154156
if ("data".equals(typeName)) {
155-
//1 byte version and 3 bytes flags; for the "well-known"
156-
//types the flags hold the value type
157+
//1 byte version and 3 bytes flags; for the "well-known" types the
158+
//flags hold the value type
157159
long valueType = reader.getUInt32() & 0xFFFFFF;
158160
reader.skip(4L);//locale
159-
totalRead += 8;
160161
int toRead = (int) fieldLen - 16;
161-
if (toRead <= 0) {
162-
//log?
163-
return;
164-
}
165-
if ("covr".equals(fieldName)) {
166-
//covr holds one image file (e.g. png or jpeg) per data
167-
//atom, and may repeat the data atom for further images
168-
handleCoverArt(reader, valueType, toRead);
169-
long remaining = recordLen - 8 - fieldLen;
170-
while (remaining >= 16) {
171-
long extraLen = reader.getUInt32();
172-
String extraTypeName = reader.getString(4, StandardCharsets.ISO_8859_1);
173-
long extraValueType = reader.getUInt32() & 0xFFFFFF;
174-
reader.skip(4L);//locale
175-
totalRead += 16;
176-
remaining -= 16;
177-
int extraToRead = (int) extraLen - 16;
178-
if (!"data".equals(extraTypeName) || extraToRead <= 0 ||
179-
extraToRead > remaining) {
180-
//malformed, skip the rest of the record
181-
break;
162+
if (toRead > 0) {
163+
if ("covr".equals(fieldName)) {
164+
//covr holds one image per data atom and may repeat the data atom
165+
//for further images; the realign below consumes any leftover
166+
handleCoverArt(reader, valueType, toRead);
167+
while (reader.getPosition() + 16 <= recordEnd) {
168+
long extraLen = reader.getUInt32();
169+
String extraType =
170+
reader.getString(4, StandardCharsets.ISO_8859_1);
171+
long extraValueType = reader.getUInt32() & 0xFFFFFF;
172+
reader.skip(4L);//locale
173+
int extraToRead = (int) extraLen - 16;
174+
if (!"data".equals(extraType) || extraToRead <= 0
175+
|| reader.getPosition() + extraToRead > recordEnd) {
176+
break;
177+
}
178+
handleCoverArt(reader, extraValueType, extraToRead);
182179
}
183-
handleCoverArt(reader, extraValueType, extraToRead);
184-
totalRead += extraToRead;
185-
remaining -= extraToRead;
186-
}
187-
if (remaining > 0) {
188-
reader.skip(remaining);
189-
totalRead += remaining;
190-
}
191-
} else if ("cpil".equals(fieldName)) {
192-
int compilationId = (int)reader.getByte();
193-
metadata.set(XMPDM.COMPILATION, compilationId);
194-
//consume the rest of the declared field: totalRead counts toRead,
195-
//but only 1 byte was read, so skip the remainder to stay aligned
196-
if (toRead > 1) {
197-
reader.skip(toRead - 1L);
198-
}
199-
} else if ("trkn".equals(fieldName)) {
200-
if (toRead == 8) {
201-
long numA = reader.getUInt32();
202-
long numB = reader.getUInt32();
203-
metadata.set(XMPDM.TRACK_NUMBER, (int)numA);
204-
//2 bytes track total, 2 bytes reserved
205-
int trackCount = (int) (numB >>> 16);
206-
if (trackCount > 0) {
207-
metadata.set(Audio.TRACK_COUNT, trackCount);
180+
} else if ("cpil".equals(fieldName)) {
181+
metadata.set(XMPDM.COMPILATION, (int) reader.getByte());
182+
} else if ("trkn".equals(fieldName)) {
183+
if (toRead >= 8) {
184+
long numA = reader.getUInt32();
185+
long numB = reader.getUInt32();
186+
metadata.set(XMPDM.TRACK_NUMBER, (int) numA);
187+
//2 bytes track total, 2 bytes reserved
188+
int trackCount = (int) (numB >>> 16);
189+
if (trackCount > 0) {
190+
metadata.set(Audio.TRACK_COUNT, trackCount);
191+
}
208192
}
209-
} else {
210-
//log
211-
reader.skip(toRead);
212-
}
213-
} else if ("disk".equals(fieldName)) {
214-
//2 bytes reserved, 2 bytes disc, 2 bytes total; some encoders
215-
//pad to 8 bytes like trkn, so consume exactly toRead either way
216-
if (toRead >= 6) {
217-
int a = reader.getInt32();
218-
short b = reader.getInt16();
219-
metadata.set(XMPDM.DISC_NUMBER, a);
220-
if (b > 0) {
221-
metadata.set(Audio.DISC_COUNT, b);
193+
} else if ("disk".equals(fieldName)) {
194+
if (toRead >= 6) {
195+
//2 bytes reserved, 2 bytes disc, 2 bytes total
196+
int a = reader.getInt32();
197+
short b = reader.getInt16();
198+
metadata.set(XMPDM.DISC_NUMBER, a);
199+
if (b > 0) {
200+
metadata.set(Audio.DISC_COUNT, b);
201+
}
222202
}
223-
reader.skip(toRead - 6);
224203
} else {
225-
reader.skip(toRead);
226-
}
227-
} else {
228-
String val = reader.getString(toRead, StandardCharsets.UTF_8);
229-
try {
230-
addMetadata(fieldName, val);
231-
} catch (SAXException e) {
232-
//need to punch through IOException catching in MP4Reader
233-
throw new RuntimeSAXException(e);
204+
String val = reader.getString(toRead, StandardCharsets.UTF_8);
205+
try {
206+
addMetadata(fieldName, val);
207+
} catch (SAXException e) {
208+
//need to punch through IOException catching in MP4Reader
209+
throw new RuntimeSAXException(e);
210+
}
234211
}
235212
}
236-
237-
totalRead += toRead;
238-
} else {
239-
int toSkip = (int) recordLen - 16;
240-
if (toSkip <= 0) {
241-
//log?
242-
return;
243-
}
244-
reader.skip(toSkip);
245-
totalRead += toSkip;
246213
}
214+
//realign to the end of the record regardless of what the branch consumed, so a
215+
//trailing sub-atom (e.g. a 'name' atom after 'data') can't desync the walk
216+
long pos = reader.getPosition();
217+
if (pos > recordEnd) {
218+
//a branch read past the record end (malformed lengths); stop
219+
return;
220+
}
221+
reader.skip(recordEnd - pos);
222+
totalRead += recordLen;
247223
}
248224
}
249225

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/video/FLVParser.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ private int readUInt24(DataInputStream input) throws IOException {
100100
//blob of deeply nested containers cannot overflow the stack (an uncaught Error)
101101
private static final int MAX_AMF_DEPTH = 64;
102102

103+
//cap the declared element count of an AMF array: it is a 32-bit field (up to
104+
//~4 billion), and each element grows a collection, so a crafted count backed by
105+
//cheap 1-byte elements would exhaust memory before EOF. Legitimate onMetaData
106+
//arrays are tiny; this only bounds the crafted case (a short read still throws).
107+
private static final int MAX_AMF_ELEMENTS = 100_000;
108+
103109
Object readAMFData(DataInputStream input, int type) throws IOException {
104110
return readAMFData(input, type, 0);
105111
}
@@ -137,6 +143,10 @@ private Object readAMFData(DataInputStream input, int type, int depth) throws IO
137143

138144
private Object readAMFStrictArray(DataInputStream input, int depth) throws IOException {
139145
long count = readUInt32(input);
146+
if (count > MAX_AMF_ELEMENTS) {
147+
throw new IOException("AMF array count " + count + " exceeds the maximum of "
148+
+ MAX_AMF_ELEMENTS);
149+
}
140150
ArrayList<Object> list = new ArrayList<>();
141151
for (int i = 0; i < count; i++) {
142152
list.add(readAMFData(input, -1, depth + 1));
@@ -167,6 +177,10 @@ private Object readAMFObject(DataInputStream input, int depth) throws IOExceptio
167177

168178
private Object readAMFEcmaArray(DataInputStream input, int depth) throws IOException {
169179
long size = readUInt32(input);
180+
if (size > MAX_AMF_ELEMENTS) {
181+
throw new IOException("AMF array size " + size + " exceeds the maximum of "
182+
+ MAX_AMF_ELEMENTS);
183+
}
170184
HashMap<String, Object> array = new HashMap<>();
171185
for (int i = 0; i < size; i++) {
172186
String key = readAMFString(input);

0 commit comments

Comments
 (0)