Skip to content

Commit 07bf18c

Browse files
committed
improve media parser robustness
1 parent f2892f5 commit 07bf18c

4 files changed

Lines changed: 61 additions & 13 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: 5 additions & 3 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;

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: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,18 @@ private int readUInt24(DataInputStream input) throws IOException {
9696
return uint;
9797
}
9898

99-
private Object readAMFData(DataInputStream input, int type) throws IOException {
99+
//AMF objects/arrays nest recursively; cap the depth so a crafted metadata
100+
//blob of deeply nested containers cannot overflow the stack (an uncaught Error)
101+
private static final int MAX_AMF_DEPTH = 64;
102+
103+
Object readAMFData(DataInputStream input, int type) throws IOException {
104+
return readAMFData(input, type, 0);
105+
}
106+
107+
private Object readAMFData(DataInputStream input, int type, int depth) throws IOException {
108+
if (depth > MAX_AMF_DEPTH) {
109+
throw new IOException("AMF nesting exceeds the maximum depth of " + MAX_AMF_DEPTH);
110+
}
100111
if (type == -1) {
101112
type = input.readUnsignedByte();
102113
}
@@ -108,11 +119,11 @@ private Object readAMFData(DataInputStream input, int type) throws IOException {
108119
case 2:
109120
return readAMFString(input);
110121
case 3:
111-
return readAMFObject(input);
122+
return readAMFObject(input, depth);
112123
case 8:
113-
return readAMFEcmaArray(input);
124+
return readAMFEcmaArray(input, depth);
114125
case 10:
115-
return readAMFStrictArray(input);
126+
return readAMFStrictArray(input, depth);
116127
case 11:
117128
final Date date = new Date((long) input.readDouble());
118129
input.readShort(); // time zone
@@ -124,11 +135,11 @@ private Object readAMFData(DataInputStream input, int type) throws IOException {
124135
}
125136
}
126137

127-
private Object readAMFStrictArray(DataInputStream input) throws IOException {
138+
private Object readAMFStrictArray(DataInputStream input, int depth) throws IOException {
128139
long count = readUInt32(input);
129140
ArrayList<Object> list = new ArrayList<>();
130141
for (int i = 0; i < count; i++) {
131-
list.add(readAMFData(input, -1));
142+
list.add(readAMFData(input, -1, depth + 1));
132143
}
133144
return list;
134145
}
@@ -141,26 +152,26 @@ private String readAMFString(DataInputStream input) throws IOException {
141152
return new String(chars, UTF_8);
142153
}
143154

144-
private Object readAMFObject(DataInputStream input) throws IOException {
155+
private Object readAMFObject(DataInputStream input, int depth) throws IOException {
145156
HashMap<String, Object> array = new HashMap<>();
146157
while (true) {
147158
String key = readAMFString(input);
148159
int dataType = input.read();
149160
if (dataType == 9) { // object end marker
150161
break;
151162
}
152-
array.put(key, readAMFData(input, dataType));
163+
array.put(key, readAMFData(input, dataType, depth + 1));
153164
}
154165
return array;
155166
}
156167

157-
private Object readAMFEcmaArray(DataInputStream input) throws IOException {
168+
private Object readAMFEcmaArray(DataInputStream input, int depth) throws IOException {
158169
long size = readUInt32(input);
159170
HashMap<String, Object> array = new HashMap<>();
160171
for (int i = 0; i < size; i++) {
161172
String key = readAMFString(input);
162173
int dataType = input.read();
163-
array.put(key, readAMFData(input, dataType));
174+
array.put(key, readAMFData(input, dataType, depth + 1));
164175
}
165176
return array;
166177
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/ID3v2FrameTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import static java.nio.charset.StandardCharsets.UTF_16BE;
2121
import static java.nio.charset.StandardCharsets.UTF_16LE;
2222
import static java.nio.charset.StandardCharsets.UTF_8;
23+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
2324
import static org.junit.jupiter.api.Assertions.assertEquals;
2425
import static org.junit.jupiter.api.Assertions.assertNull;
2526

27+
import java.io.ByteArrayInputStream;
2628
import java.io.ByteArrayOutputStream;
2729
import java.io.IOException;
2830
import java.nio.charset.Charset;
@@ -48,6 +50,17 @@ public class ID3v2FrameTest {
4850
private static final String LATIN = "Test Copyright";
4951
private static final String CJK = "日本語";
5052

53+
@Test
54+
public void testReadFullyTruncatedReturnsActualBytes() throws IOException {
55+
//a stream shorter than the declared length must yield only the bytes
56+
//present, not a zero-padded full-length array, so a truncated frame's
57+
//cover art is not emitted with trailing padding. TIKA-4812
58+
byte[] present = "abcdefghij".getBytes(ISO_8859_1); //10 bytes
59+
byte[] result = ID3v2Frame.readFully(new ByteArrayInputStream(present), 128, false);
60+
assertArrayEquals(present, result);
61+
assertEquals(present.length, result.length);
62+
}
63+
5164
private static byte[] frame(byte encodingFlag, byte[]... parts) {
5265
ByteArrayOutputStream out = new ByteArrayOutputStream();
5366
out.write(encodingFlag);

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/video/FLVParserTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
package org.apache.tika.parser.video;
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
import java.io.ByteArrayInputStream;
23+
import java.io.ByteArrayOutputStream;
24+
import java.io.DataInputStream;
25+
import java.io.IOException;
2026

2127
import org.junit.jupiter.api.Test;
2228

@@ -25,6 +31,22 @@
2531

2632
public class FLVParserTest {
2733

34+
/**
35+
* Deeply nested AMF objects used to recurse in readAMFData until the stack
36+
* overflowed (an uncaught Error); the reader must bound the nesting depth.
37+
*/
38+
@Test
39+
public void testAmfNestingIsBounded() throws Exception {
40+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
41+
bos.write(3); //top-level AMF object marker
42+
for (int i = 0; i < 100_000; i++) {
43+
bos.write(new byte[]{0, 0}); //empty key (uint16 length 0)
44+
bos.write(3); //value type = object -> recurse
45+
}
46+
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bos.toByteArray()));
47+
assertThrows(IOException.class, () -> new FLVParser().readAMFData(dis, -1));
48+
}
49+
2850
@Test
2951
public void testFLV() throws Exception {
3052
String path = "/test-documents/testFLV.flv";

0 commit comments

Comments
 (0)