Skip to content

Commit 394c737

Browse files
authored
3x-ooxml-bigdecimal-dos (#2840)
bigdecimal issue identified by @tonghuaroot
1 parent 2c84ed4 commit 394c737

2 files changed

Lines changed: 409 additions & 72 deletions

File tree

  • tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src

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

Lines changed: 204 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@
1616
*/
1717
package org.apache.tika.parser.microsoft.ooxml;
1818

19+
import java.io.InputStream;
1920
import java.math.BigDecimal;
2021
import java.util.Date;
2122
import java.util.Optional;
2223

2324
import org.apache.poi.ooxml.POIXMLProperties;
2425
import org.apache.poi.ooxml.extractor.POIXMLTextExtractor;
26+
import org.apache.poi.openxml4j.opc.OPCPackage;
27+
import org.apache.poi.openxml4j.opc.PackagePart;
28+
import org.apache.poi.openxml4j.opc.PackageRelationship;
29+
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
2530
import org.apache.poi.openxml4j.opc.internal.PackagePropertiesPart;
2631
import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor;
2732
import org.apache.xmlbeans.impl.values.XmlValueOutOfRangeException;
28-
import org.openxmlformats.schemas.officeDocument.x2006.customProperties.CTProperty;
2933
import org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.CTProperties;
34+
import org.xml.sax.Attributes;
35+
import org.xml.sax.helpers.DefaultHandler;
3036

3137
import org.apache.tika.exception.TikaException;
3238
import org.apache.tika.metadata.DublinCore;
@@ -37,10 +43,12 @@
3743
import org.apache.tika.metadata.PagedText;
3844
import org.apache.tika.metadata.Property;
3945
import org.apache.tika.metadata.TikaCoreProperties;
46+
import org.apache.tika.parser.ParseContext;
4047
import org.apache.tika.parser.microsoft.SummaryExtractor;
4148
import org.apache.tika.parser.microsoft.ooxml.xps.XPSTextExtractor;
4249
import org.apache.tika.parser.microsoft.ooxml.xslf.XSLFEventBasedPowerPointExtractor;
4350
import org.apache.tika.parser.microsoft.ooxml.xwpf.XWPFEventBasedWordExtractor;
51+
import org.apache.tika.utils.XMLReaderUtils;
4452

4553
/**
4654
* OOXML metadata extractor.
@@ -51,6 +59,28 @@
5159
*/
5260
public class MetadataExtractor {
5361

62+
private static final String CUSTOM_PROPERTIES_REL =
63+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
64+
65+
/**
66+
* Hard cap on the accumulated text-content of a single property element
67+
* inside docProps/custom.xml. Real OOXML property values are at most a few
68+
* hundred bytes; anything beyond this is either corruption or an attacker
69+
* trying to drive memory or CPU pressure (cf. the {@code <vt:decimal>}
70+
* BigDecimal DoS where a 1M-digit literal compresses ~1000:1 in deflate).
71+
* 64 KB leaves headroom for any legitimate value while bounding the
72+
* slow-path inputs decisively.
73+
*/
74+
static final int MAX_TEXT_BUFFER_LENGTH = 64 * 1024;
75+
76+
/**
77+
* Hard cap on the {@code <vt:decimal>} text length passed to
78+
* {@link BigDecimal#BigDecimal(String)}. JDK 17's parser is O(n²) in the
79+
* digit count, so even a 64 KB string costs noticeable CPU. Real-world
80+
* decimal values fit in well under 50 digits; 256 is generous.
81+
*/
82+
static final int MAX_DECIMAL_LENGTH = 256;
83+
5484
private final POIXMLTextExtractor extractor;
5585

5686
public MetadataExtractor(POIXMLTextExtractor extractor) {
@@ -65,7 +95,13 @@ public void extract(Metadata metadata) throws TikaException {
6595
extractor instanceof XPSTextExtractor) && extractor.getPackage() != null)) {
6696
extractMetadata(extractor.getCoreProperties(), metadata);
6797
extractMetadata(extractor.getExtendedProperties(), metadata);
68-
extractMetadata(extractor.getCustomProperties(), metadata);
98+
// Custom properties are read via SAX directly from the OPC part
99+
// rather than through POI/XMLBeans. The XMLBeans path materializes
100+
// an attacker-controlled <vt:decimal> through BigDecimal(String),
101+
// which is O(n²) on JDK 17 -- a 3 KB crafted carrier with a
102+
// 1,000,000-digit literal burns ~25 s of CPU before this method
103+
// even returns. See ooxml-bigdecimal-dos.
104+
extractCustomPropertiesViaSAX(extractor.getPackage(), metadata);
69105
}
70106
}
71107

@@ -157,85 +193,181 @@ private String getDocSecurityString(int docSecurityFlag) {
157193
}
158194
}
159195

160-
private void extractMetadata(POIXMLProperties.CustomProperties properties, Metadata metadata) {
161-
org.openxmlformats.schemas.officeDocument.x2006.customProperties.CTProperties props =
162-
properties.getUnderlyingProperties();
163-
for (int i = 0; i < props.sizeOfPropertyArray(); i++) {
164-
CTProperty property = props.getPropertyArray(i);
165-
String val = null;
166-
Date date = null;
167-
168-
if (property.isSetLpwstr()) {
169-
val = property.getLpwstr();
170-
} else if (property.isSetLpstr()) {
171-
val = property.getLpstr();
172-
} else if (property.isSetDate()) {
173-
date = property.getDate().getTime();
174-
} else if (property.isSetFiletime()) {
175-
date = property.getFiletime().getTime();
176-
} else if (property.isSetBool()) {
177-
val = Boolean.toString(property.getBool());
196+
/**
197+
* Parse {@code docProps/custom.xml} directly via SAX, bypassing
198+
* POI/XMLBeans. The XMLBeans path materializes an attacker-controlled
199+
* {@code <vt:decimal>} through {@link BigDecimal#BigDecimal(String)}
200+
* during XML deserialization, which is O(n²) in the digit count on
201+
* JDK 17. By reading the part ourselves we can cap both the buffered
202+
* text content ({@link #MAX_TEXT_BUFFER_LENGTH}) and the decimal
203+
* literal length ({@link #MAX_DECIMAL_LENGTH}) before any slow parse
204+
* runs.
205+
*/
206+
private void extractCustomPropertiesViaSAX(OPCPackage opcPackage, Metadata metadata) {
207+
if (opcPackage == null) {
208+
return;
209+
}
210+
try {
211+
PackagePart custPart = getRelatedPart(opcPackage, CUSTOM_PROPERTIES_REL);
212+
if (custPart == null) {
213+
return;
214+
}
215+
CustomPropertiesHandler handler = new CustomPropertiesHandler();
216+
try (InputStream is = custPart.getInputStream()) {
217+
XMLReaderUtils.parseSAX(is, handler, new ParseContext());
178218
}
219+
handler.applyTo(metadata);
220+
} catch (Exception e) {
221+
//swallow
222+
}
223+
}
179224

180-
// Integers
181-
else if (property.isSetI1()) {
182-
val = Integer.toString(property.getI1());
183-
} else if (property.isSetI2()) {
184-
val = Integer.toString(property.getI2());
185-
} else if (property.isSetI4()) {
186-
val = Integer.toString(property.getI4());
187-
} else if (property.isSetI8()) {
188-
val = Long.toString(property.getI8());
189-
} else if (property.isSetInt()) {
190-
val = Integer.toString(property.getInt());
225+
private static PackagePart getRelatedPart(OPCPackage opcPackage, String relationshipType) {
226+
try {
227+
PackageRelationshipCollection rels =
228+
opcPackage.getRelationshipsByType(relationshipType);
229+
if (rels == null || rels.size() == 0) {
230+
return null;
191231
}
232+
PackageRelationship rel = rels.getRelationship(0);
233+
if (rel == null) {
234+
return null;
235+
}
236+
return opcPackage.getPart(rel);
237+
} catch (Exception e) {
238+
return null;
239+
}
240+
}
192241

193-
// Unsigned Integers
194-
else if (property.isSetUi1()) {
195-
val = Integer.toString(property.getUi1());
196-
} else if (property.isSetUi2()) {
197-
val = Integer.toString(property.getUi2());
198-
} else if (property.isSetUi4()) {
199-
val = Long.toString(property.getUi4());
200-
} else if (property.isSetUi8()) {
201-
val = property.getUi8().toString();
202-
} else if (property.isSetUint()) {
203-
val = Long.toString(property.getUint());
242+
/**
243+
* Append SAX {@code characters()} content to {@code buf}, but stop accepting
244+
* once {@link #MAX_TEXT_BUFFER_LENGTH} is reached. Excess characters are
245+
* silently dropped; truncated values still flow through downstream parsing.
246+
*/
247+
static void appendCapped(StringBuilder buf, char[] ch, int start, int length) {
248+
if (buf.length() >= MAX_TEXT_BUFFER_LENGTH) {
249+
return;
250+
}
251+
int remaining = MAX_TEXT_BUFFER_LENGTH - buf.length();
252+
buf.append(ch, start, Math.min(length, remaining));
253+
}
254+
255+
/**
256+
* SAX handler for {@code docProps/custom.xml} (custom properties).
257+
* Matches the schema defined by Microsoft's
258+
* {@code http://schemas.openxmlformats.org/officeDocument/2006/custom-properties}
259+
* namespace, with value types coming from the {@code vt:} namespace.
260+
*/
261+
static class CustomPropertiesHandler extends DefaultHandler {
262+
263+
private static final String VT_NS =
264+
"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
265+
266+
private final Metadata customMetadata = new Metadata();
267+
private String currentPropertyName;
268+
private String currentValueType;
269+
private final StringBuilder textBuffer = new StringBuilder();
270+
271+
@Override
272+
public void startElement(String uri, String localName, String qName, Attributes atts) {
273+
if ("property".equals(localName)) {
274+
currentPropertyName = atts.getValue("name");
275+
currentValueType = null;
276+
} else if (VT_NS.equals(uri) && currentPropertyName != null
277+
&& currentValueType == null) {
278+
// Only the direct vt: child of <property> is captured.
279+
// Containers like <vt:vector>/<vt:array> latch currentValueType
280+
// here and their scalar children are then ignored, matching the
281+
// prior POI/XMLBeans behavior which skipped vectors/arrays.
282+
currentValueType = localName;
283+
textBuffer.setLength(0);
204284
}
285+
}
286+
287+
@Override
288+
public void characters(char[] ch, int start, int length) {
289+
appendCapped(textBuffer, ch, start, length);
290+
}
205291

206-
// Reals
207-
else if (property.isSetR4()) {
208-
val = Float.toString(property.getR4());
209-
} else if (property.isSetR8()) {
210-
val = Double.toString(property.getR8());
211-
} else if (property.isSetDecimal()) {
212-
BigDecimal d = property.getDecimal();
213-
if (d == null) {
214-
val = null;
215-
} else {
216-
val = d.toPlainString();
292+
@Override
293+
public void endElement(String uri, String localName, String qName) {
294+
if (VT_NS.equals(uri) && currentValueType != null &&
295+
localName.equals(currentValueType) && currentPropertyName != null) {
296+
String raw = textBuffer.toString();
297+
String trimmed = raw.trim();
298+
String propName = "custom:" + currentPropertyName;
299+
switch (currentValueType) {
300+
case "lpwstr":
301+
case "lpstr":
302+
case "bstr":
303+
// String values are user-controlled metadata content;
304+
// preserve leading/trailing whitespace as the prior
305+
// POI getLpwstr()/getLpstr() path did.
306+
customMetadata.set(propName, raw);
307+
break;
308+
case "filetime":
309+
case "date":
310+
Property tikaProp = Property.externalDate(propName);
311+
customMetadata.set(tikaProp, trimmed);
312+
break;
313+
case "bool":
314+
// xs:boolean lexical space allows "1"/"0" alongside
315+
// "true"/"false"; the prior POI path emitted
316+
// Boolean.toString(...). Preserve that normalization.
317+
if ("1".equals(trimmed) || "true".equalsIgnoreCase(trimmed)) {
318+
customMetadata.set(propName, "true");
319+
} else if ("0".equals(trimmed) || "false".equalsIgnoreCase(trimmed)) {
320+
customMetadata.set(propName, "false");
321+
}
322+
break;
323+
case "i1":
324+
case "i2":
325+
case "i4":
326+
case "int":
327+
case "ui1":
328+
case "ui2":
329+
customMetadata.set(propName, trimmed);
330+
break;
331+
case "i8":
332+
case "ui4":
333+
case "ui8":
334+
case "uint":
335+
customMetadata.set(propName, trimmed);
336+
break;
337+
case "r4":
338+
case "r8":
339+
customMetadata.set(propName, trimmed);
340+
break;
341+
case "decimal":
342+
// BigDecimal(String) is O(n²) on JDK 17; cap the input
343+
// length to keep an attacker-controlled <vt:decimal>
344+
// from burning CPU. Real values are < 50 chars; 256 is
345+
// generous. See ooxml-bigdecimal-dos.
346+
if (trimmed.length() > MAX_DECIMAL_LENGTH) {
347+
break;
348+
}
349+
try {
350+
BigDecimal d = new BigDecimal(trimmed);
351+
customMetadata.set(propName, d.toPlainString());
352+
} catch (NumberFormatException e) {
353+
//swallow
354+
}
355+
break;
356+
default:
357+
break;
217358
}
218-
} else if (property.isSetArray()) {
219-
// TODO Fetch the array values and output
220-
} else if (property.isSetVector()) {
221-
// TODO Fetch the vector values and output
222-
} else if (property.isSetBlob() || property.isSetOblob()) {
223-
// TODO Decode, if possible
224-
} else if (property.isSetStream() || property.isSetOstream() ||
225-
property.isSetVstream()) {
226-
// TODO Decode, if possible
227-
} else if (property.isSetStorage() || property.isSetOstorage()) {
228-
// TODO Decode, if possible
229-
} else {
230-
// This type isn't currently supported yet, skip the property
359+
currentValueType = null;
360+
} else if ("property".equals(localName)) {
361+
currentPropertyName = null;
362+
currentValueType = null;
231363
}
364+
}
232365

233-
String propName = "custom:" + property.getName();
234-
if (date != null) {
235-
Property tikaProp = Property.externalDate(propName);
236-
metadata.set(tikaProp, date);
237-
} else if (val != null) {
238-
metadata.set(propName, val);
366+
void applyTo(Metadata metadata) {
367+
for (String name : customMetadata.names()) {
368+
for (String value : customMetadata.getValues(name)) {
369+
metadata.add(name, value);
370+
}
239371
}
240372
}
241373
}

0 commit comments

Comments
 (0)