diff --git a/poi-integration/src/test/java/org/apache/poi/stress/TestAllFiles.java b/poi-integration/src/test/java/org/apache/poi/stress/TestAllFiles.java index b3aff917ac..957b804ff5 100644 --- a/poi-integration/src/test/java/org/apache/poi/stress/TestAllFiles.java +++ b/poi-integration/src/test/java/org/apache/poi/stress/TestAllFiles.java @@ -97,6 +97,9 @@ public class TestAllFiles { "poifs/60320-protected.xlsx", "poifs/protected_sha512.xlsx", + // stress docs + "document/deep-table-cell.docx", + // NOTE: Expected failures should usually be added in file "stress.xls" instead // of being listed here in order to also verify the expected exception details! }; diff --git a/poi-ooxml/src/main/java/org/apache/poi/ooxml/POIXMLException.java b/poi-ooxml/src/main/java/org/apache/poi/ooxml/POIXMLException.java index f949eccb90..be53202057 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/ooxml/POIXMLException.java +++ b/poi-ooxml/src/main/java/org/apache/poi/ooxml/POIXMLException.java @@ -19,7 +19,7 @@ package org.apache.poi.ooxml; /** * Indicates a generic OOXML error. */ -public final class POIXMLException extends RuntimeException{ +public final class POIXMLException extends RuntimeException { /** * Create a new {@code POIXMLException} with no * detail message. diff --git a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java index eb8c4c30c9..56db1b0420 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java +++ b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java @@ -23,22 +23,11 @@ import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.Spliterator; +import java.util.*; import javax.xml.namespace.QName; import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; import org.apache.logging.log4j.Logger; +import org.apache.poi.POIException; import org.apache.poi.logging.PoiLogManager; import org.apache.poi.common.usermodel.PictureType; import org.apache.poi.ooxml.POIXMLDocument; @@ -61,6 +50,7 @@ import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.util.IOUtils; import org.apache.poi.util.Internal; import org.apache.poi.util.Removal; +import org.apache.poi.util.XMLHelper; import org.apache.poi.wp.usermodel.HeaderFooterType; import org.apache.poi.xddf.usermodel.chart.XDDFChart; import org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy; @@ -105,6 +95,7 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.StylesDocument; @SuppressWarnings("unused") public class XWPFDocument extends POIXMLDocument implements Document, IBody { private static final Logger LOG = PoiLogManager.getLogger(XWPFDocument.class); + private static final int MAX_NODE_DEPTH = 1000; protected List footers = new ArrayList<>(); protected List headers = new ArrayList<>(); @@ -214,6 +205,13 @@ public class XWPFDocument extends POIXMLDocument implements Document, IBody { doc = DocumentDocument.Factory.parse(stream, DEFAULT_XML_OPTIONS); ctDocument = doc.getDocument(); } + final int nodeDepth = XMLHelper.getDepthOfChildNodes(ctDocument.getDomNode(), MAX_NODE_DEPTH); + if (nodeDepth > MAX_NODE_DEPTH) { + throw new IOException(String.format(Locale.ROOT, + "The document is too complex, it has a node depth of %s, which exceeds the maximum allowed of %s", + nodeDepth, + MAX_NODE_DEPTH)); + } initFootnotes(); @@ -304,6 +302,8 @@ public class XWPFDocument extends POIXMLDocument implements Document, IBody { } } initHyperlinks(); + } catch (POIException e) { + throw new IOException(e); } catch (XmlException e) { throw new POIXMLException(e); } diff --git a/poi-ooxml/src/test/java/org/apache/poi/xwpf/TestXWPFBugs.java b/poi-ooxml/src/test/java/org/apache/poi/xwpf/TestXWPFBugs.java index c39c7fe7cd..170473396e 100644 --- a/poi-ooxml/src/test/java/org/apache/poi/xwpf/TestXWPFBugs.java +++ b/poi-ooxml/src/test/java/org/apache/poi/xwpf/TestXWPFBugs.java @@ -28,6 +28,7 @@ import javax.crypto.Cipher; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.poi.POIDataSamples; +import org.apache.poi.POIException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackagePartName; @@ -274,4 +275,13 @@ class TestXWPFBugs { assertEquals(STJcTable.END, tbl5.getCTTbl().getTblPr().getJc().xgetVal().getEnumValue()); } } + + @Test + public void testDeepTableCell() throws Exception { + // Document contains a table with nested cells. + IOException ex = assertThrows(IOException.class, + () -> XWPFTestDataSamples.openSampleDocument("deep-table-cell.docx")); + assertInstanceOf(POIException.class, ex.getCause()); + assertTrue(ex.getMessage().contains("Node depth exceeds maximum supported depth")); + } } diff --git a/poi/src/main/java/org/apache/poi/POIException.java b/poi/src/main/java/org/apache/poi/POIException.java new file mode 100644 index 0000000000..106f82596a --- /dev/null +++ b/poi/src/main/java/org/apache/poi/POIException.java @@ -0,0 +1,59 @@ +/* ==================================================================== + 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.poi; + +/** + * Indicates a generic POI exception. This is not commonly used in POI + * but this is intended to be a base class for some new POI exceptions. + * Historically, POI has used {@link RuntimeException} for most of its + * exceptions, but this is not a good practice. This class is a checked + * class that extends {@link Exception} so needs to be explictly + * caught or declared in the method signature. + * + * @since POI 5.5.0 + */ +public class POIException extends Exception { + private static final long serialVersionUID = 1L; + + /** + * Create a new {@code POIException} with the specified message. + * + * @param msg The error message for the exception. + */ + public POIException(String msg) { + super(msg); + } + + /** + * Create a new {@code POIException} with the specified cause. + * + * @param cause the cause of this exception + */ + public POIException(Throwable cause) { + super(cause); + } + + /** + * Create a new {@code POIException} with the specified message and cause. + * + * @param msg The error message for the exception. + * @param cause the cause of this exception + */ + public POIException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/poi/src/main/java/org/apache/poi/util/XMLHelper.java b/poi/src/main/java/org/apache/poi/util/XMLHelper.java index efeca688a5..071ae08f54 100644 --- a/poi/src/main/java/org/apache/poi/util/XMLHelper.java +++ b/poi/src/main/java/org/apache/poi/util/XMLHelper.java @@ -30,6 +30,7 @@ import static javax.xml.stream.XMLOutputFactory.IS_REPAIRING_NAMESPACES; import java.io.StringReader; import java.lang.reflect.Method; +import java.util.Locale; import java.util.concurrent.TimeUnit; import javax.xml.parsers.DocumentBuilder; @@ -49,7 +50,9 @@ import javax.xml.validation.SchemaFactory; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogBuilder; import org.apache.logging.log4j.Logger; +import org.apache.poi.POIException; import org.apache.poi.logging.PoiLogManager; +import org.w3c.dom.Node; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -77,7 +80,6 @@ public final class XMLHelper { "org.apache.xerces.util.SecurityManager" }; - private static final Logger LOG = PoiLogManager.getLogger(XMLHelper.class); private static long lastLog; @@ -253,6 +255,38 @@ public final class XMLHelper { return factory; } + /** + * Counts the depth of the DOM tree starting from the given node. + * + * @param node the node to check + * @param maxSupportedDepth the maximum supported depth of the DOM tree + * @return the depth + * @throws POIException if the depth exceeds maxSupportedDepth + */ + public static int getDepthOfChildNodes(final Node node, final int maxSupportedDepth) throws POIException { + return getDepthOfChildNodes(node, maxSupportedDepth, 0); + } + + private static int getDepthOfChildNodes(final Node node, final int maxSupportedDepth, + final int nodeDepth) throws POIException { + final int currentDepth = nodeDepth + 1; + int maxDepth = currentDepth; + Node child = node.getFirstChild(); + while (child != null) { + int childDepth = getDepthOfChildNodes(child, maxSupportedDepth, currentDepth); + if (childDepth > maxDepth) { + maxDepth = childDepth; + if (maxDepth > maxSupportedDepth) { + throw new POIException(String.format(Locale.ROOT, + "Node depth exceeds maximum supported depth of %s" , + maxSupportedDepth)); + } + } + child = child.getNextSibling(); + } + return maxDepth; + } + private static Object _xercesSecurityManager; private static volatile boolean _xercesSecurityManagerSet = false; diff --git a/test-data/document/deep-table-cell.docx b/test-data/document/deep-table-cell.docx new file mode 100644 index 0000000000..6bb54e6c6d Binary files /dev/null and b/test-data/document/deep-table-cell.docx differ