This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new bea45c9e53 TIKA-4861: reject a BigTIFF directory offset that overflows
the prefi… (#3130)
bea45c9e53 is described below
commit bea45c9e53aae7a943d3abd293f228af80ed77a3
Author: Tim Allison <[email protected]>
AuthorDate: Fri Sep 4 15:10:03 2026 -0400
TIKA-4861: reject a BigTIFF directory offset that overflows the prefi…
(#3130)
---
CHANGES.txt | 9 ++
.../apache/tika/detect/image/RawTiffDetector.java | 7 +-
.../tika/detect/image/RawTiffDetectorFuzzTest.java | 148 +++++++++++++++++++++
.../tika/detect/image/RawTiffDetectorTest.java | 54 +++++++-
4 files changed, 216 insertions(+), 2 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index f15a629c07..10f3fc3ddc 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,14 @@
Release 4.1.0 - unreleased
+ * RawTiffDetector rejects a BigTIFF directory offset near Long.MAX_VALUE
+ instead of letting the bounds check overflow. Adding the entry-count
+ size to such an offset wrapped negative and read as "already in the
+ prefix", so a 16-byte file threw ArrayIndexOutOfBoundsException out of
+ Detector.detect, which CompositeDetector does not catch: detection
+ failed for the document and the remaining detectors never ran. Raw
+ detection runs on every stream, so this was reachable from every entry
+ point (TIKA-4861).
+
* Entries of ODF, EPUB, GeoGebra, WACZ, XLZ and iWork containers, mbox
messages and the AppleSingle data fork are re-opened from their
container on rewind instead of cached: digesting rewinds every
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java
index 8ba5a6379a..4521287a77 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/detect/image/RawTiffDetector.java
@@ -161,10 +161,15 @@ public class RawTiffDetector implements Detector {
* @return whether {@code buf[0..end)} is valid now
*/
boolean ensure(long end) throws IOException {
+ //callers add to a 64-bit offset taken from the file, so end can
wrap
+ //negative; reject that before the length comparison lets it
through
+ if (end < 0 || end > limit) {
+ return false;
+ }
if (end <= length) {
return true;
}
- if (in == null || end > limit) {
+ if (in == null) {
return false;
}
int wanted = (int) Math.min(limit, ((end + CHUNK_LENGTH - 1) /
CHUNK_LENGTH) * CHUNK_LENGTH);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorFuzzTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorFuzzTest.java
new file mode 100644
index 0000000000..0237153761
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorFuzzTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.detect.image;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.Locale;
+import java.util.Random;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Randomized boundary test for {@link RawTiffDetector}'s directory walk.
+ * <p>
+ * The detector runs ahead of every parser, and {@code Detector.detect}
declares
+ * only {@link java.io.IOException}: anything else it throws aborts detection
for
+ * the document and the remaining detectors never run. So the invariant is
simply
+ * that no throwable escapes, whatever the directories say.
+ * <p>
+ * Inputs are well-formed TIFF and BigTIFF headers whose offsets, counts and
+ * entry values are drawn from the arithmetic boundaries -- 0, the 32 and 64
bit
+ * maxima, the prefix limit, and their neighbours -- since that is where the
+ * offset handling goes wrong rather than in random bytes. The seed is random
per
+ * run and reported on failure.
+ * <p>
+ * Each input goes through both entry points: the in-memory one, and the stream
+ * one, which is the only way to reach the chunked reads in {@code
Prefix.ensure}
+ * -- the arithmetic this guards. Trials are cheap but they saturate: measured
+ * against this generator, 2000 reaches the same branches 20000 does.
+ */
+public class RawTiffDetectorFuzzTest {
+
+ private static final int TRIALS = 2000;
+
+ /**
+ * Offsets and values worth trying: adding an entry size to one of the
large
+ * ones overflows, which is the arithmetic under test.
+ */
+ private static final long[] BOUNDARIES = {
+ 0L, 1L, 8L, 16L, 0xFFFFL, 0x7FFFFFFFL, 0x80000000L, 0xFFFFFFFFL,
0x100000000L,
+ Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 7,
Long.MAX_VALUE - 8,
+ Long.MAX_VALUE - 20, Long.MIN_VALUE, -1L,
+ RawTiffDetector.MAX_PREFIX_LENGTH,
RawTiffDetector.MAX_PREFIX_LENGTH - 1,
+ RawTiffDetector.MAX_PREFIX_LENGTH + 1};
+
+ private static final int[] TAGS =
+ {0x00FE, 0x0102, 0x0103, 0x0106, 0x010F, 0x014A, 0xC612};
+ private static final int[] TYPES = {2, 3, 4, 13, 16, 18};
+
+ @Test
+ public void testBoundaryOffsets() {
+ long seed = new Random().nextLong();
+ Random rng = new Random(seed);
+ for (int trial = 0; trial < TRIALS; trial++) {
+ byte[] tiff = randomTiff(rng);
+ try {
+ RawTiffDetector.detect(tiff, tiff.length);
+ try (TikaInputStream tis = TikaInputStream.get(tiff)) {
+ new RawTiffDetector().detect(tis, new Metadata(), new
ParseContext());
+ }
+ } catch (Throwable t) {
+ fail("detect threw " + t + " -- seed=" + seed + " trial=" +
trial
+ + " bytes=" + hex(tiff), t);
+ }
+ }
+ }
+
+ private static byte[] randomTiff(Random rng) {
+ boolean bigTiff = rng.nextInt(4) != 0;
+ byte[] b = new byte[24 + rng.nextInt(400)];
+ rng.nextBytes(b);
+ b[0] = 'I';
+ b[1] = 'I';
+ put(b, 2, bigTiff ? 43 : 42, 2);
+ int header;
+ if (bigTiff) {
+ put(b, 4, 8, 2);
+ put(b, 6, 0, 2);
+ put(b, 8, boundary(rng), 8);
+ header = 16;
+ } else {
+ put(b, 4, boundary(rng), 4);
+ header = 8;
+ }
+ if (rng.nextBoolean()) {
+ //also point the header at a directory that is really there, so the
+ //entry values get walked rather than rejected at the first offset
+ put(b, bigTiff ? 8 : 4, header, bigTiff ? 8 : 4);
+ fillDirectory(b, header, bigTiff, rng);
+ }
+ return b;
+ }
+
+ private static void fillDirectory(byte[] b, int at, boolean bigTiff,
Random rng) {
+ int countSize = bigTiff ? 8 : 2;
+ int entrySize = bigTiff ? 20 : 12;
+ int offsetSize = bigTiff ? 8 : 4;
+ int numEntries = rng.nextInt(6);
+ put(b, at, numEntries, countSize);
+ int p = at + countSize;
+ for (int i = 0; i < numEntries && p + entrySize <= b.length; i++) {
+ put(b, p, TAGS[rng.nextInt(TAGS.length)], 2);
+ put(b, p + 2, TYPES[rng.nextInt(TYPES.length)], 2);
+ put(b, p + 4, boundary(rng), offsetSize);
+ put(b, p + 4 + offsetSize, boundary(rng), offsetSize);
+ p += entrySize;
+ }
+ if (p + offsetSize <= b.length) {
+ put(b, p, boundary(rng), offsetSize);
+ }
+ }
+
+ private static long boundary(Random rng) {
+ return BOUNDARIES[rng.nextInt(BOUNDARIES.length)];
+ }
+
+ private static void put(byte[] b, int off, long value, int width) {
+ for (int i = 0; i < width && off + i < b.length; i++) {
+ b[off + i] = (byte) ((value >>> (8 * i)) & 0xFF);
+ }
+ }
+
+ private static String hex(byte[] b) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < Math.min(b.length, 64); i++) {
+ sb.append(String.format(Locale.ROOT, "%02X", b[i]));
+ }
+ return sb.toString();
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java
index 16fa1472a1..58cf342305 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/detect/image/RawTiffDetectorTest.java
@@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
import org.apache.tika.detect.DefaultDetector;
import org.apache.tika.detect.Detector;
@@ -224,6 +225,50 @@ public class RawTiffDetectorTest {
}
}
+ /**
+ * A BigTIFF directory offset near {@code Long.MAX_VALUE}: adding the entry
+ * count to it wraps negative, and a negative end must not read as "already
+ * in the prefix". The three pointer sources (this header field, a SubIFDs
+ * array, the follower below) all reach the same bounds check.
+ */
+ @ParameterizedTest
+ @ValueSource(longs = {Long.MAX_VALUE, Long.MAX_VALUE - 7, Long.MAX_VALUE -
8,
+ 0x100000000L, 1024L * 1024L + 1})
+ public void testDirectoryOffsetBeyondTheFileIsRejected(long firstIfd)
throws Exception {
+ byte[] tiff = bigTiffHeader(firstIfd);
+ assertEquals(MediaType.OCTET_STREAM, RawTiffDetector.detect(tiff,
tiff.length));
+ try (TikaInputStream tis = TikaInputStream.get(tiff)) {
+ assertEquals(MediaType.OCTET_STREAM,
+ new RawTiffDetector().detect(tis, new Metadata(), new
ParseContext()));
+ }
+ }
+
+ /**
+ * The same offset as the follower of an otherwise good directory: the
+ * follower is skipped and the vendor named in the directory already read
+ * still decides the type.
+ */
+ @Test
+ public void testFollowerBeyondTheFileIsSkipped() {
+ byte[] tiff = new TiffBuilder(true)
+ .ifd(entry(0x0103, 3, 32767))
+ .nextOffset(Long.MAX_VALUE)
+ .build();
+ assertEquals(RawTiffDetector.SONY, RawTiffDetector.detect(tiff,
tiff.length));
+ }
+
+ /**
+ * A 16 byte little-endian BigTIFF header, directories nowhere near it.
+ */
+ private static byte[] bigTiffHeader(long firstIfd) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.writeBytes(new byte[]{'I', 'I', 43, 0});
+ le16(out, 8);
+ le16(out, 0);
+ le64(out, firstIfd);
+ return out.toByteArray();
+ }
+
/**
* A minimal little-endian TIFF: one IFD with Make, Compression,
* PhotometricInterpretation and, optionally, DNGVersion.
@@ -288,6 +333,7 @@ public class RawTiffDetectorTest {
private final boolean bigTiff;
private final java.util.List<Entry[]> ifds = new
java.util.ArrayList<>();
private boolean nextPointsToSelf;
+ private Long nextOffset;
private final java.util.Map<Integer, Integer> gaps = new
java.util.HashMap<>();
/**
@@ -312,6 +358,12 @@ public class RawTiffDetectorTest {
return this;
}
+ /** The follower of every IFD, in place of the default 0. */
+ TiffBuilder nextOffset(long offset) {
+ nextOffset = offset;
+ return this;
+ }
+
byte[] build() {
int headerSize = bigTiff ? 16 : 8;
int countSize = bigTiff ? 8 : 2;
@@ -389,7 +441,7 @@ public class RawTiffDetectorTest {
}
}
}
- long next = nextPointsToSelf ? starts[i] : 0;
+ long next = nextOffset != null ? nextOffset : nextPointsToSelf
? starts[i] : 0;
offset(out, next, offsetSize);
out.writeBytes(data.toByteArray());
}