Skip to content

Commit 298144e

Browse files
authored
TIKA-4878: never treat a declared Content-Length as a measurement (#3131)
1 parent b8ed3fb commit 298144e

5 files changed

Lines changed: 67 additions & 11 deletions

File tree

CHANGES.txt

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

3+
* A declared Content-Length is no longer treated as a measurement: the
4+
zip-bomb ratio counts only measured input bytes (a container-declared
5+
size on an embedded document could inflate its denominator), and a
6+
re-openable source no longer reserves cache budget or sizes its buffer
7+
from the declared length (a lying one could push a small payload to
8+
disk or churn the shared budget). Neither is in a release: the
9+
exposure arrived with TIKA-4868 and TIKA-4873 (TIKA-4878).
310
* Embedded objects in Office documents are re-opened from their container
411
instead of cached: every OOXML part (pictures, media, attachments), the
512
OLE 2.0 package inside an OOXML part, the CONTENTS entry of an OLE 2.0

tika-core/src/main/java/org/apache/tika/io/ReopenableSource.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,23 +221,19 @@ private void maybeReleaseRetained() {
221221
/**
222222
* Drains a fresh stream into memory if it fits within the per-object floor plus what
223223
* can be reserved from the shared budget, retaining the buffer (and its reservation)
224-
* until {@link #close()}. The declared length is a sizing hint only -- it can lie, so
225-
* the cap is enforced during the read. Does not disturb this source's read position.
224+
* until {@link #close()}. The declared length is the file's claim: it may skip an
225+
* attempt that cannot succeed (a lie there costs a spill, nothing more), but it never
226+
* sizes a reservation or an allocation -- the buffer starts at the floor at most and
227+
* grows, reserving, on what is actually read. Does not disturb this source's read
228+
* position.
226229
*/
227230
private boolean tryBufferInMemory() throws IOException {
228231
if (length > MAX_ARRAY_SIZE || (length > IN_MEMORY_FLOOR && budget == null)) {
229232
return false;
230233
}
231234
long reservedHere = 0;
232235
// Reservation invariant: reservedHere == max(0, data.length - IN_MEMORY_FLOOR)
233-
if (length > IN_MEMORY_FLOOR) {
234-
long extra = length - IN_MEMORY_FLOOR;
235-
if (budget.tryReserve(extra) != extra) {
236-
return false;
237-
}
238-
reservedHere = extra;
239-
}
240-
byte[] data = new byte[length > 0 ? (int) length : 8192];
236+
byte[] data = new byte[(int) Math.max(8192, Math.min(length, IN_MEMORY_FLOOR))];
241237
int total = 0;
242238
boolean fits = false;
243239
try (InputStream in = opener.get()) {

tika-core/src/main/java/org/apache/tika/sax/SecureContentHandler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,9 @@ public void throwIfCauseOf(SAXException e) throws TikaException {
212212

213213
private long getByteCount() throws SAXException {
214214
try {
215-
if (stream.hasLength()) {
215+
//a declared Content-Length is the file's claim; only a measured length
216+
//or the bytes actually read can serve as the ratio's denominator
217+
if (stream.hasReliableLength()) {
216218
return stream.getLength();
217219
} else {
218220
return stream.getPosition();

tika-core/src/test/java/org/apache/tika/io/ReopenableSourceTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,29 @@ public void testChannelBudgetExhaustedSpills() throws Exception {
245245
}
246246
}
247247

248+
/**
249+
* A declared length far above the content, against a budget that could never
250+
* grant it: the claim must not be what gets reserved, or a 500-byte payload is
251+
* pushed to disk by a number the file made up.
252+
*/
253+
@Test
254+
public void testLyingDeclaredLengthDoesNotReserveOrSpill() throws Exception {
255+
byte[] data = data(500);
256+
AtomicInteger opens = new AtomicInteger();
257+
CacheMemoryBudget budget = new CacheMemoryBudget(1024);
258+
try (ReopenableSource source = new ReopenableSource(countingOpener(data, opens), tmp,
259+
50L * 1024 * 1024, null)) {
260+
source.enableRewind(budget);
261+
try (SeekableByteChannel channel = source.getSeekableByteChannel()) {
262+
assertInstanceOf(MemorySeekableByteChannel.class, channel);
263+
assertArrayEquals(data, readFully(channel));
264+
}
265+
assertFalse(source.hasPath());
266+
assertEquals(0, budget.getReservedBytes(), "nothing reserved for a 500 byte payload");
267+
assertEquals(500, source.getLength());
268+
}
269+
}
270+
248271
@Test
249272
public void testLyingDeclaredLengthCorrected() throws Exception {
250273
byte[] data = data(500);

tika-core/src/test/java/org/apache/tika/sax/SecureContentHandlerTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
import org.xml.sax.helpers.DefaultHandler;
3030

3131
import org.apache.tika.exception.TikaException;
32+
import org.apache.tika.io.TemporaryResources;
3233
import org.apache.tika.io.TikaInputStream;
34+
import org.apache.tika.metadata.HttpHeaders;
35+
import org.apache.tika.metadata.Metadata;
3336

3437
/**
3538
* Tests for the {@link SecureContentHandler} class.
@@ -108,6 +111,31 @@ public void testManyCharactersPerByte() throws IOException {
108111
}
109112
}
110113

114+
/**
115+
* A one-shot stream carrying a declared Content-Length far above what it holds:
116+
* the claim must not become the input byte count, or the ratio never trips.
117+
*/
118+
@Test
119+
public void testDeclaredLengthDoesNotInflateByteCount() throws IOException {
120+
Metadata metadata = new Metadata();
121+
//a plausible lie: large enough that no ratio trips, small enough that
122+
//byteCount * ratio does not overflow and trip for the wrong reason
123+
metadata.set(HttpHeaders.CONTENT_LENGTH, Long.toString(100L * 1024 * 1024 * 1024));
124+
try (TikaInputStream lying = TikaInputStream.get(new NullInputStream(MANY_BYTES),
125+
new TemporaryResources(), metadata)) {
126+
SecureContentHandler lyingHandler =
127+
new SecureContentHandler(new DefaultHandler(), lying);
128+
char[] ch = new char[1000];
129+
for (int i = 0; i < MANY_BYTES; i++) {
130+
lying.read();
131+
lyingHandler.characters(ch, 0, ch.length);
132+
}
133+
fail("Expected SAXException not thrown");
134+
} catch (SAXException e) {
135+
// expected
136+
}
137+
}
138+
111139
@Test
112140
public void testSomeCharactersWithoutInput() throws IOException {
113141
try {

0 commit comments

Comments
 (0)