Skip to content

Commit bea45c9

Browse files
authored
TIKA-4861: reject a BigTIFF directory offset that overflows the prefi… (#3130)
1 parent da7e4d8 commit bea45c9

4 files changed

Lines changed: 216 additions & 2 deletions

File tree

CHANGES.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
Release 4.1.0 - unreleased
22

3+
* RawTiffDetector rejects a BigTIFF directory offset near Long.MAX_VALUE
4+
instead of letting the bounds check overflow. Adding the entry-count
5+
size to such an offset wrapped negative and read as "already in the
6+
prefix", so a 16-byte file threw ArrayIndexOutOfBoundsException out of
7+
Detector.detect, which CompositeDetector does not catch: detection
8+
failed for the document and the remaining detectors never ran. Raw
9+
detection runs on every stream, so this was reachable from every entry
10+
point (TIKA-4861).
11+
312
* Entries of ODF, EPUB, GeoGebra, WACZ, XLZ and iWork containers, mbox
413
messages and the AppleSingle data fork are re-opened from their
514
container on rewind instead of cached: digesting rewinds every

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,15 @@ static final class Prefix {
161161
* @return whether {@code buf[0..end)} is valid now
162162
*/
163163
boolean ensure(long end) throws IOException {
164+
//callers add to a 64-bit offset taken from the file, so end can wrap
165+
//negative; reject that before the length comparison lets it through
166+
if (end < 0 || end > limit) {
167+
return false;
168+
}
164169
if (end <= length) {
165170
return true;
166171
}
167-
if (in == null || end > limit) {
172+
if (in == null) {
168173
return false;
169174
}
170175
int wanted = (int) Math.min(limit, ((end + CHUNK_LENGTH - 1) / CHUNK_LENGTH) * CHUNK_LENGTH);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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.detect.image;
18+
19+
import static org.junit.jupiter.api.Assertions.fail;
20+
21+
import java.util.Locale;
22+
import java.util.Random;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.apache.tika.io.TikaInputStream;
27+
import org.apache.tika.metadata.Metadata;
28+
import org.apache.tika.parser.ParseContext;
29+
30+
/**
31+
* Randomized boundary test for {@link RawTiffDetector}'s directory walk.
32+
* <p>
33+
* The detector runs ahead of every parser, and {@code Detector.detect} declares
34+
* only {@link java.io.IOException}: anything else it throws aborts detection for
35+
* the document and the remaining detectors never run. So the invariant is simply
36+
* that no throwable escapes, whatever the directories say.
37+
* <p>
38+
* Inputs are well-formed TIFF and BigTIFF headers whose offsets, counts and
39+
* entry values are drawn from the arithmetic boundaries -- 0, the 32 and 64 bit
40+
* maxima, the prefix limit, and their neighbours -- since that is where the
41+
* offset handling goes wrong rather than in random bytes. The seed is random per
42+
* run and reported on failure.
43+
* <p>
44+
* Each input goes through both entry points: the in-memory one, and the stream
45+
* one, which is the only way to reach the chunked reads in {@code Prefix.ensure}
46+
* -- the arithmetic this guards. Trials are cheap but they saturate: measured
47+
* against this generator, 2000 reaches the same branches 20000 does.
48+
*/
49+
public class RawTiffDetectorFuzzTest {
50+
51+
private static final int TRIALS = 2000;
52+
53+
/**
54+
* Offsets and values worth trying: adding an entry size to one of the large
55+
* ones overflows, which is the arithmetic under test.
56+
*/
57+
private static final long[] BOUNDARIES = {
58+
0L, 1L, 8L, 16L, 0xFFFFL, 0x7FFFFFFFL, 0x80000000L, 0xFFFFFFFFL, 0x100000000L,
59+
Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 7, Long.MAX_VALUE - 8,
60+
Long.MAX_VALUE - 20, Long.MIN_VALUE, -1L,
61+
RawTiffDetector.MAX_PREFIX_LENGTH, RawTiffDetector.MAX_PREFIX_LENGTH - 1,
62+
RawTiffDetector.MAX_PREFIX_LENGTH + 1};
63+
64+
private static final int[] TAGS =
65+
{0x00FE, 0x0102, 0x0103, 0x0106, 0x010F, 0x014A, 0xC612};
66+
private static final int[] TYPES = {2, 3, 4, 13, 16, 18};
67+
68+
@Test
69+
public void testBoundaryOffsets() {
70+
long seed = new Random().nextLong();
71+
Random rng = new Random(seed);
72+
for (int trial = 0; trial < TRIALS; trial++) {
73+
byte[] tiff = randomTiff(rng);
74+
try {
75+
RawTiffDetector.detect(tiff, tiff.length);
76+
try (TikaInputStream tis = TikaInputStream.get(tiff)) {
77+
new RawTiffDetector().detect(tis, new Metadata(), new ParseContext());
78+
}
79+
} catch (Throwable t) {
80+
fail("detect threw " + t + " -- seed=" + seed + " trial=" + trial
81+
+ " bytes=" + hex(tiff), t);
82+
}
83+
}
84+
}
85+
86+
private static byte[] randomTiff(Random rng) {
87+
boolean bigTiff = rng.nextInt(4) != 0;
88+
byte[] b = new byte[24 + rng.nextInt(400)];
89+
rng.nextBytes(b);
90+
b[0] = 'I';
91+
b[1] = 'I';
92+
put(b, 2, bigTiff ? 43 : 42, 2);
93+
int header;
94+
if (bigTiff) {
95+
put(b, 4, 8, 2);
96+
put(b, 6, 0, 2);
97+
put(b, 8, boundary(rng), 8);
98+
header = 16;
99+
} else {
100+
put(b, 4, boundary(rng), 4);
101+
header = 8;
102+
}
103+
if (rng.nextBoolean()) {
104+
//also point the header at a directory that is really there, so the
105+
//entry values get walked rather than rejected at the first offset
106+
put(b, bigTiff ? 8 : 4, header, bigTiff ? 8 : 4);
107+
fillDirectory(b, header, bigTiff, rng);
108+
}
109+
return b;
110+
}
111+
112+
private static void fillDirectory(byte[] b, int at, boolean bigTiff, Random rng) {
113+
int countSize = bigTiff ? 8 : 2;
114+
int entrySize = bigTiff ? 20 : 12;
115+
int offsetSize = bigTiff ? 8 : 4;
116+
int numEntries = rng.nextInt(6);
117+
put(b, at, numEntries, countSize);
118+
int p = at + countSize;
119+
for (int i = 0; i < numEntries && p + entrySize <= b.length; i++) {
120+
put(b, p, TAGS[rng.nextInt(TAGS.length)], 2);
121+
put(b, p + 2, TYPES[rng.nextInt(TYPES.length)], 2);
122+
put(b, p + 4, boundary(rng), offsetSize);
123+
put(b, p + 4 + offsetSize, boundary(rng), offsetSize);
124+
p += entrySize;
125+
}
126+
if (p + offsetSize <= b.length) {
127+
put(b, p, boundary(rng), offsetSize);
128+
}
129+
}
130+
131+
private static long boundary(Random rng) {
132+
return BOUNDARIES[rng.nextInt(BOUNDARIES.length)];
133+
}
134+
135+
private static void put(byte[] b, int off, long value, int width) {
136+
for (int i = 0; i < width && off + i < b.length; i++) {
137+
b[off + i] = (byte) ((value >>> (8 * i)) & 0xFF);
138+
}
139+
}
140+
141+
private static String hex(byte[] b) {
142+
StringBuilder sb = new StringBuilder();
143+
for (int i = 0; i < Math.min(b.length, 64); i++) {
144+
sb.append(String.format(Locale.ROOT, "%02X", b[i]));
145+
}
146+
return sb.toString();
147+
}
148+
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.junit.jupiter.api.Test;
2626
import org.junit.jupiter.params.ParameterizedTest;
2727
import org.junit.jupiter.params.provider.CsvSource;
28+
import org.junit.jupiter.params.provider.ValueSource;
2829

2930
import org.apache.tika.detect.DefaultDetector;
3031
import org.apache.tika.detect.Detector;
@@ -224,6 +225,50 @@ public void testTruncatedPrefixIsHarmless() {
224225
}
225226
}
226227

228+
/**
229+
* A BigTIFF directory offset near {@code Long.MAX_VALUE}: adding the entry
230+
* count to it wraps negative, and a negative end must not read as "already
231+
* in the prefix". The three pointer sources (this header field, a SubIFDs
232+
* array, the follower below) all reach the same bounds check.
233+
*/
234+
@ParameterizedTest
235+
@ValueSource(longs = {Long.MAX_VALUE, Long.MAX_VALUE - 7, Long.MAX_VALUE - 8,
236+
0x100000000L, 1024L * 1024L + 1})
237+
public void testDirectoryOffsetBeyondTheFileIsRejected(long firstIfd) throws Exception {
238+
byte[] tiff = bigTiffHeader(firstIfd);
239+
assertEquals(MediaType.OCTET_STREAM, RawTiffDetector.detect(tiff, tiff.length));
240+
try (TikaInputStream tis = TikaInputStream.get(tiff)) {
241+
assertEquals(MediaType.OCTET_STREAM,
242+
new RawTiffDetector().detect(tis, new Metadata(), new ParseContext()));
243+
}
244+
}
245+
246+
/**
247+
* The same offset as the follower of an otherwise good directory: the
248+
* follower is skipped and the vendor named in the directory already read
249+
* still decides the type.
250+
*/
251+
@Test
252+
public void testFollowerBeyondTheFileIsSkipped() {
253+
byte[] tiff = new TiffBuilder(true)
254+
.ifd(entry(0x0103, 3, 32767))
255+
.nextOffset(Long.MAX_VALUE)
256+
.build();
257+
assertEquals(RawTiffDetector.SONY, RawTiffDetector.detect(tiff, tiff.length));
258+
}
259+
260+
/**
261+
* A 16 byte little-endian BigTIFF header, directories nowhere near it.
262+
*/
263+
private static byte[] bigTiffHeader(long firstIfd) {
264+
ByteArrayOutputStream out = new ByteArrayOutputStream();
265+
out.writeBytes(new byte[]{'I', 'I', 43, 0});
266+
le16(out, 8);
267+
le16(out, 0);
268+
le64(out, firstIfd);
269+
return out.toByteArray();
270+
}
271+
227272
/**
228273
* A minimal little-endian TIFF: one IFD with Make, Compression,
229274
* PhotometricInterpretation and, optionally, DNGVersion.
@@ -288,6 +333,7 @@ private static final class TiffBuilder {
288333
private final boolean bigTiff;
289334
private final java.util.List<Entry[]> ifds = new java.util.ArrayList<>();
290335
private boolean nextPointsToSelf;
336+
private Long nextOffset;
291337
private final java.util.Map<Integer, Integer> gaps = new java.util.HashMap<>();
292338

293339
/**
@@ -312,6 +358,12 @@ TiffBuilder nextPointsToSelf() {
312358
return this;
313359
}
314360

361+
/** The follower of every IFD, in place of the default 0. */
362+
TiffBuilder nextOffset(long offset) {
363+
nextOffset = offset;
364+
return this;
365+
}
366+
315367
byte[] build() {
316368
int headerSize = bigTiff ? 16 : 8;
317369
int countSize = bigTiff ? 8 : 2;
@@ -389,7 +441,7 @@ byte[] build() {
389441
}
390442
}
391443
}
392-
long next = nextPointsToSelf ? starts[i] : 0;
444+
long next = nextOffset != null ? nextOffset : nextPointsToSelf ? starts[i] : 0;
393445
offset(out, next, offsetSize);
394446
out.writeBytes(data.toByteArray());
395447
}

0 commit comments

Comments
 (0)