Skip to content

Commit 5dbfb15

Browse files
authored
TIKA-4533 -- fix handling of TikaInputStreams with open containers (#2378)
1 parent 2021e71 commit 5dbfb15

9 files changed

Lines changed: 192 additions & 21 deletions

File tree

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,26 @@ public static TikaInputStream get(InputStream stream, TemporaryResources tmp, Me
245245
}
246246
}
247247

248+
/**
249+
* Use this if there is no actual underlying InputStream. It is important
250+
* to set a length so that the zip bomb detector won't be triggered
251+
* in the SecurityHandler.
252+
* <p>
253+
* If your stream has underlying bytes and a length, see {@link #setOpenContainer(Object)}
254+
*
255+
* @param openContainer
256+
* @param length
257+
* @param metadata
258+
* @return
259+
*/
260+
public static TikaInputStream getFromContainer(Object openContainer, long length, Metadata metadata) {
261+
TikaInputStream tis = TikaInputStream.get(new byte[0], metadata);
262+
tis.setOpenContainer(openContainer);
263+
//this overwrites the length that was set in the constructor above
264+
tis.setLength(length);
265+
return tis;
266+
}
267+
248268
/**
249269
* Casts or wraps the given stream to a TikaInputStream instance.
250270
* This method can be used to access the functionality of this class
@@ -668,6 +688,10 @@ public Object getOpenContainer() {
668688
* the stream, eg after a Zip contents
669689
* detector has loaded the file to decide
670690
* what it contains.
691+
* <p>
692+
* If there's no undelrying stream, consider {@link #getFromContainer(Object, long, Metadata)}
693+
* because that will avoid potential improper zip bomb exceptions from the SecurityHandler if
694+
* it thinks the length of the stream == 0.
671695
*/
672696
public void setOpenContainer(Object container) {
673697
openContainer = container;
@@ -818,6 +842,16 @@ public long getPosition() {
818842
return position;
819843
}
820844

845+
/**
846+
* This should only be called by the constructor for an open container with a 0 length
847+
* byte inputStream
848+
*
849+
* @param length
850+
*/
851+
private void setLength(long length) {
852+
this.length = length;
853+
}
854+
821855
/**
822856
* This relies on {@link IOUtils#skip(InputStream, long, byte[])} to ensure
823857
* that the alleged bytes skipped were actually skipped.

tika-core/src/main/java/org/apache/tika/parser/DigestingParser.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,24 @@
2020

2121
import java.io.IOException;
2222
import java.io.InputStream;
23+
import java.io.OutputStream;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
2326

2427
import org.xml.sax.ContentHandler;
2528
import org.xml.sax.SAXException;
2629

2730
import org.apache.tika.exception.TikaException;
31+
import org.apache.tika.extractor.DefaultEmbeddedStreamTranslator;
32+
import org.apache.tika.extractor.EmbeddedStreamTranslator;
2833
import org.apache.tika.io.TemporaryResources;
2934
import org.apache.tika.io.TikaInputStream;
3035
import org.apache.tika.metadata.Metadata;
3136
import org.apache.tika.metadata.TikaCoreProperties;
3237

3338
public class DigestingParser extends ParserDecorator {
3439

40+
private final EmbeddedStreamTranslator embeddedStreamTranslator = new DefaultEmbeddedStreamTranslator();
3541
private final Digester digester;
3642
private final boolean skipContainerDocument;
3743
/**
@@ -48,10 +54,25 @@ public DigestingParser(Parser parser, Digester digester, boolean skipContainerDo
4854
@Override
4955
public void parse(InputStream stream, ContentHandler handler, Metadata metadata,
5056
ParseContext context) throws IOException, SAXException, TikaException {
57+
58+
59+
if (! shouldDigest(metadata)) {
60+
super.parse(stream, handler, metadata, context);
61+
return;
62+
}
5163
TemporaryResources tmp = new TemporaryResources();
5264
TikaInputStream tis = TikaInputStream.get(stream, tmp, metadata);
5365
try {
54-
if (shouldDigest(metadata)) {
66+
67+
if (embeddedStreamTranslator.shouldTranslate(tis, metadata)) {
68+
Path tmpBytes = tmp.createTempFile();
69+
try (OutputStream os = Files.newOutputStream(tmpBytes)) {
70+
embeddedStreamTranslator.translate(tis, metadata, os);
71+
}
72+
try (TikaInputStream translated = TikaInputStream.get(tmpBytes)) {
73+
digester.digest(translated, metadata, context);
74+
}
75+
} else {
5576
digester.digest(tis, metadata, context);
5677
}
5778
super.parse(tis, handler, metadata, context);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,11 @@ private long getByteCount() throws SAXException {
208208
*/
209209
protected void advance(int length) throws SAXException {
210210
characterCount += length;
211-
long byteCount = getByteCount();
212-
if (characterCount > threshold && characterCount > byteCount * ratio) {
213-
throw new SecureSAXException(
214-
"Suspected zip bomb: " + byteCount + " input bytes produced " + characterCount +
215-
" output characters");
211+
if (characterCount > threshold) {
212+
long byteCount = getByteCount();
213+
if (characterCount > byteCount * ratio) {
214+
throw new SecureSAXException("Suspected zip bomb: " + byteCount + " input bytes produced " + characterCount + " output characters");
215+
}
216216
}
217217
}
218218

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractPOIFSExtractor.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.IOException;
2121
import java.io.InputStream;
2222
import java.nio.charset.StandardCharsets;
23+
import java.util.Iterator;
2324

2425
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
2526
import org.apache.poi.hpsf.ClassID;
@@ -194,7 +195,6 @@ protected void handleEmbeddedOfficeDoc(DirectoryEntry dir, Metadata metadata,
194195
}
195196

196197
// It's regular OLE2:
197-
198198
// What kind of document is it?
199199
metadata.set(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID, dir.getName());
200200
if (dir.getStorageClsid() != null) {
@@ -237,6 +237,18 @@ protected void handleEmbeddedOfficeDoc(DirectoryEntry dir, Metadata metadata,
237237
}
238238
}
239239

240+
private long estimateSize(DirectoryEntry dir) {
241+
Iterator<Entry> entries = dir.getEntries();
242+
long sz = 0;
243+
while (entries.hasNext()) {
244+
Entry entry = entries.next();
245+
if (entry.isDocumentEntry()) {
246+
sz += ((DocumentEntry)entry).getSize();
247+
}
248+
}
249+
return sz;
250+
}
251+
240252
private void extractOCXName(DirectoryEntry dir, Metadata metadata) {
241253
if (! dir.hasEntry(OCX_NAME)) {
242254
return;
@@ -266,14 +278,14 @@ private void extractOCXName(DirectoryEntry dir, Metadata metadata) {
266278
}
267279
}
268280

269-
private void handleCompObj(DirectoryEntry dir, POIFSDocumentType type, String rName,
281+
private void handleCompObj(DirectoryEntry parentDir, POIFSDocumentType type, String rName,
270282
Metadata metadata, XHTMLContentHandler xhtml, boolean outputHtml)
271283
throws IOException, SAXException {
272284
//TODO: figure out if the equivalent of OLE 1.0's
273285
//getCommand() and getFileName() exist for OLE 2.0 to populate
274286
//TikaCoreProperties.ORIGINAL_RESOURCE_NAME
275287

276-
String contentsEntryName = getContentsEntryName(dir);
288+
String contentsEntryName = getContentsEntryName(parentDir);
277289
if (contentsEntryName == null) {
278290
//log or record exception?
279291
return;
@@ -282,7 +294,7 @@ private void handleCompObj(DirectoryEntry dir, POIFSDocumentType type, String rN
282294
DocumentEntry contentsEntry;
283295

284296
try {
285-
contentsEntry = (DocumentEntry) dir.getEntry(contentsEntryName);
297+
contentsEntry = (DocumentEntry) parentDir.getEntry(contentsEntryName);
286298
} catch (FileNotFoundException fnfe) {
287299
EmbeddedDocumentUtil.recordEmbeddedStreamException(fnfe, parentMetadata);
288300
return;
@@ -314,7 +326,7 @@ private void handleCompObj(DirectoryEntry dir, POIFSDocumentType type, String rN
314326
metadata.set(Metadata.CONTENT_TYPE, mediaType.getType());
315327
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, rName + extension);
316328
metadata.set(Metadata.CONTENT_LENGTH, Integer.toString(length));
317-
parseEmbedded(dir, tis, xhtml, metadata, outputHtml);
329+
parseEmbedded(parentDir, tis, xhtml, metadata, outputHtml);
318330
} finally {
319331
inp.close();
320332
}
@@ -374,15 +386,15 @@ private void handleOLENative(DirectoryEntry dir, POIFSDocumentType type, String
374386
}
375387
}
376388

377-
private void parseEmbedded(DirectoryEntry dir, TikaInputStream tis, XHTMLContentHandler xhtml,
389+
private void parseEmbedded(DirectoryEntry parentDir, TikaInputStream tis, XHTMLContentHandler xhtml,
378390
Metadata metadata, boolean outputHtml) throws IOException,
379391
SAXException {
380392
if (!embeddedDocumentUtil.shouldParseEmbedded(metadata)) {
381393
return;
382394
}
383-
if (dir.getStorageClsid() != null) {
395+
if (parentDir.getStorageClsid() != null) {
384396
metadata.set(Office.EMBEDDED_STORAGE_CLASS_ID,
385-
dir.getStorageClsid().toString());
397+
parentDir.getStorageClsid().toString());
386398
}
387399
embeddedDocumentUtil.parseEmbedded(tis, xhtml, metadata, outputHtml);
388400
}
@@ -393,8 +405,8 @@ private void parseEmbedded(DirectoryEntry dir, XHTMLContentHandler xhtml, Metada
393405
if (!embeddedDocumentUtil.shouldParseEmbedded(metadata)) {
394406
return;
395407
}
396-
try (TikaInputStream tis = TikaInputStream.get(new byte[0])) {
397-
tis.setOpenContainer(dir);
408+
long sz = estimateSize(dir);
409+
try (TikaInputStream tis = TikaInputStream.getFromContainer(dir, sz, metadata)) {
398410
if (dir.getStorageClsid() != null) {
399411
metadata.set(Office.EMBEDDED_STORAGE_CLASS_ID,
400412
dir.getStorageClsid().toString());

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/pst/OutlookPSTParser.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.InputStream;
2424
import java.util.Set;
2525

26+
import com.pff.PSTException;
2627
import com.pff.PSTFile;
2728
import com.pff.PSTFolder;
2829
import com.pff.PSTMessage;
@@ -114,9 +115,9 @@ private void parseFolder(XHTMLContentHandler handler, PSTFolder pstFolder, Strin
114115
Metadata metadata = new Metadata();
115116
metadata.set(TikaCoreProperties.CONTENT_TYPE_PARSER_OVERRIDE, PSTMailItemParser.PST_MAIL_ITEM_STRING);
116117
metadata.set(PST.PST_FOLDER_PATH, folderPath);
117-
try (TikaInputStream tis = TikaInputStream.get(new byte[0])) {
118-
tis.setOpenContainer(pstMail);
119-
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, pstMail.getSubject() + ".msg");
118+
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, pstMail.getSubject() + ".msg");
119+
long length = estimateSize(pstMail);
120+
try (TikaInputStream tis = TikaInputStream.getFromContainer(pstMail, length, metadata)) {
120121
embeddedExtractor.parseEmbedded(tis, handler, metadata, true);
121122
}
122123
pstMail = (PSTMessage) pstFolder.getNextChild();
@@ -134,4 +135,28 @@ private void parseFolder(XHTMLContentHandler handler, PSTFolder pstFolder, Strin
134135
}
135136
}
136137
}
138+
139+
static protected long estimateSize(PSTMessage attachedEmail) {
140+
//we do this for a rough estimate of email body size
141+
//so that we don't get a zip bomb exception on exceedingly large msgs.
142+
long sz = 0;
143+
sz += getStringLength(attachedEmail.getBody());
144+
try {
145+
sz += getStringLength(attachedEmail.getRTFBody());
146+
} catch (PSTException | IOException e) {
147+
//swallow
148+
}
149+
sz += getStringLength(attachedEmail.getBodyHTML());
150+
sz += getStringLength(attachedEmail.getSubject());
151+
//complete heuristic to account for from, to, etc...
152+
sz += 100_000;
153+
return sz;
154+
}
155+
156+
private static long getStringLength(String s) {
157+
if (s == null) {
158+
return 0;
159+
}
160+
return s.length();
161+
}
137162
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/pst/PSTMailItemParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ private void parseMailAttachment(XHTMLContentHandler xhtml, PSTAttachment attach
224224
PSTMessage attachedEmail = attachment.getEmbeddedPSTMessage();
225225
//check for whether this is a binary attachment or an embedded pst msg
226226
if (attachedEmail != null) {
227-
try (TikaInputStream tis = TikaInputStream.get(new byte[0])) {
228-
tis.setOpenContainer(attachedEmail);
227+
long sz = OutlookPSTParser.estimateSize(attachedEmail);
228+
try (TikaInputStream tis = TikaInputStream.getFromContainer(attachedEmail, sz, metadata)) {
229229
Metadata attachMetadata = new Metadata();
230230
attachMetadata.set(TikaCoreProperties.CONTENT_TYPE_PARSER_OVERRIDE, PSTMailItemParser.PST_MAIL_ITEM_STRING);
231231
attachMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, attachedEmail.getSubject() + ".msg");

tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/AutoDetectParserTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static java.nio.charset.StandardCharsets.UTF_8;
2020
import static org.junit.jupiter.api.Assertions.assertEquals;
2121
import static org.junit.jupiter.api.Assertions.assertNotNull;
22+
import static org.junit.jupiter.api.Assertions.assertNull;
2223
import static org.junit.jupiter.api.Assertions.assertTrue;
2324
import static org.junit.jupiter.api.Assertions.fail;
2425

@@ -27,6 +28,8 @@
2728
import java.io.IOException;
2829
import java.io.InputStream;
2930
import java.util.HashSet;
31+
import java.util.List;
32+
import java.util.Locale;
3033
import java.util.Set;
3134
import java.util.zip.ZipEntry;
3235
import java.util.zip.ZipOutputStream;
@@ -48,6 +51,7 @@
4851
import org.apache.tika.metadata.TikaCoreProperties;
4952
import org.apache.tika.metadata.XMPDM;
5053
import org.apache.tika.mime.MediaType;
54+
import org.apache.tika.parser.digestutils.CommonsDigester;
5155
import org.apache.tika.parser.external.CompositeExternalParser;
5256
import org.apache.tika.sax.BodyContentHandler;
5357
import org.apache.tika.sax.ToXMLContentHandler;
@@ -556,4 +560,32 @@ public String toString() {
556560
" expectedContentFragment = " + expectedContentFragment + "\n";
557561
}
558562
}
563+
564+
@Test
565+
public void testLargeEmbeddedOle2Object() throws Exception {
566+
List<Metadata> metadataList = getRecursiveMetadata("testLargeOLEDoc.doc");
567+
assertEquals(3, metadataList.size());
568+
assertNull(metadataList.get(2).get(TikaCoreProperties.EMBEDDED_EXCEPTION));
569+
}
570+
571+
@Test
572+
public void testDigestingOpenContainers() throws Exception {
573+
String expectedSha = "bbc2057a1ff8fe859a296d2fbb493fc0c3e5796749ba72507c0e13f7a3d81f78";
574+
TikaConfig tikaConfig = null;
575+
try (InputStream is = AutoDetectParserTest.class.getResourceAsStream("/configs/tika-4533.xml")) {
576+
tikaConfig = new TikaConfig(is);
577+
}
578+
Parser parser = new AutoDetectParser(tikaConfig);
579+
List<Metadata> metadataList = getRecursiveMetadata("testLargeOLEDoc.doc", parser, new ParseContext());
580+
assertEquals(expectedSha, metadataList.get(2).get("X-TIKA:digest:SHA256"));
581+
582+
//now test that we get the same digest if we warp the auto detect parser vs configuring it
583+
Parser autoDetectParser = new AutoDetectParser();
584+
Parser digestingParser = new DigestingParser(autoDetectParser, new CommonsDigester(10000, "SHA256"), true);
585+
586+
metadataList = getRecursiveMetadata("testLargeOLEDoc.doc", digestingParser, new ParseContext());
587+
assertEquals(expectedSha, metadataList.get(2).get("X-TIKA:digest:SHA256").toLowerCase(Locale.US));
588+
589+
590+
}
559591
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one or more
4+
contributor license agreements. See the NOTICE file distributed with
5+
this work for additional information regarding copyright ownership.
6+
The ASF licenses this file to You under the Apache License, Version 2.0
7+
(the "License"); you may not use this file except in compliance with
8+
the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-->
18+
<properties>
19+
<autoDetectParserConfig>
20+
<params>
21+
<!-- if the incoming metadata object has a ContentLength entry and it is larger than this
22+
value, spool the file to disk; this is useful for some file formats that are more efficiently
23+
processed via a file instead of an InputStream -->
24+
<spoolToDisk>0</spoolToDisk>
25+
<!-- the next four are parameters for the SecureContentHandler -->
26+
<!-- threshold used in zip bomb detection. This many characters must be written
27+
before the maximum compression ratio is calculated -->
28+
<outputThreshold>10000</outputThreshold>
29+
<!-- maximum compression ratio between output characters and input bytes -->
30+
<maximumCompressionRatio>100</maximumCompressionRatio>
31+
<!-- maximum XML element nesting level -->
32+
<maximumDepth>100</maximumDepth>
33+
<!-- maximum embedded file depth -->
34+
<maximumPackageEntryDepth>100</maximumPackageEntryDepth>
35+
<!-- throw an exception if a file has zero bytes -->
36+
<throwOnZeroBytes>false</throwOnZeroBytes>
37+
</params>
38+
<!-- as of Tika 2.5.x, this is the preferred way to configure digests -->
39+
<digesterFactory class="org.apache.tika.parser.digestutils.CommonsDigesterFactory">
40+
<params>
41+
<markLimit>100000</markLimit>
42+
<!-- this specifies SHA256, base32 and MD5 -->
43+
<algorithmString>sha256</algorithmString>
44+
</params>
45+
</digesterFactory>
46+
</autoDetectParserConfig>
47+
</properties>

0 commit comments

Comments
 (0)