From 444dc1b64c015197bf6b5219da9459bc0cf6f787 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Wed, 10 Sep 2025 16:40:27 +0100 Subject: Support disabling external file access --- .../org/zwobble/mammoth/DocumentConverter.java | 9 +++ .../internal/InternalDocumentConverter.java | 4 +- .../internal/conversion/DocumentToHtmlOptions.java | 88 ++++++++++++++++++++-- .../mammoth/internal/docx/DisabledFileReader.java | 11 +++ .../mammoth/internal/docx/DocumentReader.java | 12 ++- .../org/zwobble/mammoth/tests/MammothTests.java | 23 ++++++ .../mammoth/tests/docx/DocumentReaderTests.java | 55 +++++++++----- 7 files changed, 173 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/zwobble/mammoth/internal/docx/DisabledFileReader.java (limited to 'src') diff --git a/src/main/java/org/zwobble/mammoth/DocumentConverter.java b/src/main/java/org/zwobble/mammoth/DocumentConverter.java index 2dea8e8..c7dca20 100644 --- a/src/main/java/org/zwobble/mammoth/DocumentConverter.java +++ b/src/main/java/org/zwobble/mammoth/DocumentConverter.java @@ -60,6 +60,15 @@ public class DocumentConverter { return new DocumentConverter(options.disableEmbeddedStyleMap()); } + /** + * Source documents may reference files outside of the source document. + * Call this to disable access to any such external files during the conversion process. + * This is highly recommended when converting untrusted user input. + */ + public DocumentConverter disableExternalFileAccess() { + return new DocumentConverter(options.disableExternalFileAccess()); + } + /** * By default, images are converted to {@code } elements with the source included inline in the {@code src} attribute. * Call this to change how images are converted. diff --git a/src/main/java/org/zwobble/mammoth/internal/InternalDocumentConverter.java b/src/main/java/org/zwobble/mammoth/internal/InternalDocumentConverter.java index 8ff1e95..3e01181 100644 --- a/src/main/java/org/zwobble/mammoth/internal/InternalDocumentConverter.java +++ b/src/main/java/org/zwobble/mammoth/internal/InternalDocumentConverter.java @@ -45,7 +45,7 @@ public class InternalDocumentConverter { Optional styleMap = readEmbeddedStyleMap(zipFile).map(StyleMapParser::parse); DocumentToHtmlOptions conversionOptions = styleMap.map(options::addEmbeddedStyleMap).orElse(options); - return readDocument(path, zipFile) + return readDocument(path, zipFile, conversionOptions.externalFileAccess()) .flatMap(nodes -> DocumentToHtml.convertToHtml(nodes, conversionOptions)) .map(Html::stripEmpty) .map(Html::collapse) @@ -69,7 +69,7 @@ public class InternalDocumentConverter { } private InternalResult extractRawText(Optional path, Archive zipFile) { - return readDocument(path, zipFile) + return readDocument(path, zipFile, options.externalFileAccess()) .map(RawText::extractRawText); } diff --git a/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java index 2c455e2..d12bbd4 100644 --- a/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java +++ b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java @@ -16,6 +16,7 @@ public class DocumentToHtmlOptions { StyleMap.EMPTY, false, false, + false, InternalImageConverter.imgElement(image -> { String base64 = Base64Encoding.streamToBase64(image::getInputStream); String src = "data:" + image.getContentType() + ";base64," + base64; @@ -30,6 +31,7 @@ public class DocumentToHtmlOptions { private final StyleMap embeddedStyleMap; private final boolean disableDefaultStyleMap; private final boolean disableEmbeddedStyleMap; + private final boolean disableExternalFileAccess; private final InternalImageConverter imageConverter; public DocumentToHtmlOptions( @@ -39,6 +41,7 @@ public class DocumentToHtmlOptions { StyleMap embeddedStyleMap, boolean disableDefaultStyleMap, boolean disableEmbeddedStyleMap, + boolean disableExternalFileAccess, InternalImageConverter imageConverter ) { this.idPrefix = idPrefix; @@ -47,15 +50,34 @@ public class DocumentToHtmlOptions { this.embeddedStyleMap = embeddedStyleMap; this.disableDefaultStyleMap = disableDefaultStyleMap; this.disableEmbeddedStyleMap = disableEmbeddedStyleMap; + this.disableExternalFileAccess = disableExternalFileAccess; this.imageConverter = imageConverter; } public DocumentToHtmlOptions idPrefix(String prefix) { - return new DocumentToHtmlOptions(prefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); + return new DocumentToHtmlOptions( + prefix, + preserveEmptyParagraphs, + styleMap, + embeddedStyleMap, + disableDefaultStyleMap, + disableEmbeddedStyleMap, + disableExternalFileAccess, + imageConverter + ); } public DocumentToHtmlOptions preserveEmptyParagraphs() { - return new DocumentToHtmlOptions(idPrefix, true, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); + return new DocumentToHtmlOptions( + idPrefix, + true, + styleMap, + embeddedStyleMap, + disableDefaultStyleMap, + disableEmbeddedStyleMap, + disableExternalFileAccess, + imageConverter + ); } public DocumentToHtmlOptions addStyleMap(String styleMap) { @@ -63,19 +85,68 @@ public class DocumentToHtmlOptions { } public DocumentToHtmlOptions addStyleMap(StyleMap styleMap) { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, this.styleMap.update(styleMap), embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); + return new DocumentToHtmlOptions( + idPrefix, + preserveEmptyParagraphs, + this.styleMap.update(styleMap), + embeddedStyleMap, + disableDefaultStyleMap, + disableEmbeddedStyleMap, + disableExternalFileAccess, + imageConverter + ); } public DocumentToHtmlOptions disableDefaultStyleMap() { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, true, disableEmbeddedStyleMap, imageConverter); + return new DocumentToHtmlOptions( + idPrefix, + preserveEmptyParagraphs, + styleMap, + embeddedStyleMap, + true, + disableEmbeddedStyleMap, + disableExternalFileAccess, + imageConverter + ); } public DocumentToHtmlOptions disableEmbeddedStyleMap() { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, true, imageConverter); + return new DocumentToHtmlOptions( + idPrefix, + preserveEmptyParagraphs, + styleMap, + embeddedStyleMap, + disableDefaultStyleMap, + true, + disableExternalFileAccess, + imageConverter + ); + } + + public DocumentToHtmlOptions disableExternalFileAccess() { + return new DocumentToHtmlOptions( + idPrefix, + preserveEmptyParagraphs, + styleMap, + embeddedStyleMap, + disableDefaultStyleMap, + disableEmbeddedStyleMap, + true, + imageConverter + ); } public DocumentToHtmlOptions addEmbeddedStyleMap(StyleMap embeddedStyleMap) { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); + return new DocumentToHtmlOptions( + idPrefix, + preserveEmptyParagraphs, + styleMap, + embeddedStyleMap, + disableDefaultStyleMap, + disableEmbeddedStyleMap, + disableExternalFileAccess, + imageConverter + ); } public DocumentToHtmlOptions imageConverter(ImageConverter.ImgElement imageConverter) { @@ -86,6 +157,7 @@ public class DocumentToHtmlOptions { embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, + disableExternalFileAccess, InternalImageConverter.imgElement(imageConverter) ); } @@ -110,6 +182,10 @@ public class DocumentToHtmlOptions { return styleMap; } + public boolean externalFileAccess() { + return !this.disableExternalFileAccess; + } + public InternalImageConverter imageConverter() { return imageConverter; } diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DisabledFileReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/DisabledFileReader.java new file mode 100644 index 0000000..03386d4 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/internal/docx/DisabledFileReader.java @@ -0,0 +1,11 @@ +package org.zwobble.mammoth.internal.docx; + +import java.io.IOException; +import java.io.InputStream; + +public class DisabledFileReader implements FileReader { + @Override + public InputStream getInputStream(String uri) throws IOException { + throw new IOException("could not open external image '" + uri + "': external file access is disabled"); + } +} diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java index 1955e55..495646f 100644 --- a/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java +++ b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java @@ -22,13 +22,21 @@ import static org.zwobble.mammoth.internal.util.Lists.*; import static org.zwobble.mammoth.internal.util.Strings.trimLeft; public class DocumentReader { - public static InternalResult readDocument(Optional path, Archive zipFile) { + public static InternalResult readDocument( + Optional path, + Archive zipFile, + boolean externalFileAccess + ) { PartPaths partPaths = findPartPaths(zipFile); Styles styles = readStyles(zipFile, partPaths); Numbering numbering = readNumbering(zipFile, partPaths, styles); ContentTypes contentTypes = readContentTypes(zipFile); - FileReader fileReader = new PathRelativeFileReader(path); + + FileReader fileReader = externalFileAccess + ? new PathRelativeFileReader(path) + : new DisabledFileReader(); + PartWithBodyReader partReader = new PartWithBodyReader(zipFile, contentTypes, fileReader, numbering, styles); return InternalResult.flatMap( readNotes(partReader, partPaths), diff --git a/src/test/java/org/zwobble/mammoth/tests/MammothTests.java b/src/test/java/org/zwobble/mammoth/tests/MammothTests.java index 85a20ce..a072185 100644 --- a/src/test/java/org/zwobble/mammoth/tests/MammothTests.java +++ b/src/test/java/org/zwobble/mammoth/tests/MammothTests.java @@ -120,6 +120,29 @@ public class MammothTests { } } + @Test + public void warnIfDocumentHasImagesStoredOutsideOfDocumentWhenExternalFileAccessIsDisabled() throws IOException { + Path tempDirectory = Files.createTempDirectory("mammoth-"); + try { + Path documentPath = tempDirectory.resolve("external-picture.docx"); + Files.copy(TestData.file("external-picture.docx").toPath(), documentPath); + Files.copy(TestData.file("tiny-picture.png").toPath(), tempDirectory.resolve("tiny-picture.png")); + assertThat( + new DocumentConverter() + .disableExternalFileAccess() + .convertToHtml(documentPath.toFile()), + allOf( + hasProperty("value", equalTo("")), + hasProperty("warnings", contains( + equalTo("could not open external image 'tiny-picture.png': external file access is disabled") + )) + ) + ); + } finally { + tempDirectory.toFile().delete(); + } + } + @Test public void warnIfImagesStoredOutsideOfDocumentAreNotFound() throws IOException { Path tempDirectory = Files.createTempDirectory("mammoth-"); diff --git a/src/test/java/org/zwobble/mammoth/tests/docx/DocumentReaderTests.java b/src/test/java/org/zwobble/mammoth/tests/docx/DocumentReaderTests.java index 1c07679..db24404 100644 --- a/src/test/java/org/zwobble/mammoth/tests/docx/DocumentReaderTests.java +++ b/src/test/java/org/zwobble/mammoth/tests/docx/DocumentReaderTests.java @@ -38,30 +38,47 @@ public class DocumentReaderTests { @Test public void mainDocumentIsFoundUsingPackageRelationships() { - InternalResult result = DocumentReader.readDocument(Optional.empty(), InMemoryArchive.fromStrings(map( + Archive archive = InMemoryArchive.fromStrings(map( "word/document2.xml", XmlWriter.toString( - element("w:document", list( - element("w:body", list( - element("w:p", list( - element("w:r", list( - element("w:t", list(text("Hello."))) - )) - )) - )) - )), + element( + "w:document", list( + element( + "w:body", list( + element( + "w:p", list( + element( + "w:r", list( + element("w:t", list(text("Hello."))) + ) + ) + ) + ) + ) + ) + ) + ), mainDocumentNamespaces ), "_rels/.rels", XmlWriter.toString( - element("Relationships", list( - element("Relationship", map( - "Id", "rId1", - "Target", "/word/document2.xml", - "Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" - )) - )), + element( + "Relationships", list( + element( + "Relationship", map( + "Id", "rId1", + "Target", "/word/document2.xml", + "Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" + ) + ) + ) + ), relationshipsNamespaces ) - ))); + )); + InternalResult result = DocumentReader.readDocument( + Optional.empty(), + archive, + false + ); assertThat(result, isInternalSuccess(document( withChildren(paragraphWithText("Hello.")) @@ -84,7 +101,7 @@ public class DocumentReaderTests { )); PassThroughException exception = assertThrows( PassThroughException.class, - () -> DocumentReader.readDocument(Optional.empty(), archive) + () -> DocumentReader.readDocument(Optional.empty(), archive, false) ); assertThat(exception.getMessage(), equalTo("java.io.IOException: Could not find main document part. Are you sure this is a valid .docx file?")); } -- cgit v1.2.3