From 0f07f6ff1342cac954e6ff3138d2eeb0049ce441 Mon Sep 17 00:00:00 2001 From: jmestwa-coder Date: Tue, 12 May 2026 19:48:23 +0530 Subject: [PATCH] Reject path traversal filenames during HMEF attachment extraction --- .../hmef/extractor/HMEFContentsExtractor.java | 8 +++- .../extractor/TestHMEFContentsExtractor.java | 47 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hmef/extractor/HMEFContentsExtractor.java b/poi-scratchpad/src/main/java/org/apache/poi/hmef/extractor/HMEFContentsExtractor.java index 8e01252c647..65e7612da83 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hmef/extractor/HMEFContentsExtractor.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hmef/extractor/HMEFContentsExtractor.java @@ -22,6 +22,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Path; import org.apache.poi.hmef.Attachment; import org.apache.poi.hmef.HMEFMessage; @@ -30,6 +31,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more import org.apache.poi.hmef.attribute.MAPIStringAttribute; import org.apache.poi.hsmf.datatypes.MAPIProperty; import org.apache.poi.hsmf.datatypes.Types; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.StringUtil; /** @@ -152,10 +154,14 @@ public void extractAttachments(File dir) throws IOException { } // Save it - File file = new File(dir, filename); + File file = getOutputFile(dir, filename); try (OutputStream fout = Files.newOutputStream(file.toPath())) { fout.write(att.getContents()); } } } + + private static File getOutputFile(File dir, String filename) throws IOException { + return IOUtils.newFile(dir, filename); + } } diff --git a/poi-scratchpad/src/test/java/org/apache/poi/hmef/extractor/TestHMEFContentsExtractor.java b/poi-scratchpad/src/test/java/org/apache/poi/hmef/extractor/TestHMEFContentsExtractor.java index 9f41088668c..8a82a041bd1 100644 --- a/poi-scratchpad/src/test/java/org/apache/poi/hmef/extractor/TestHMEFContentsExtractor.java +++ b/poi-scratchpad/src/test/java/org/apache/poi/hmef/extractor/TestHMEFContentsExtractor.java @@ -19,16 +19,24 @@ Licensed to the Apache Software Foundation (ASF) under one or more import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.Arrays; import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; import org.apache.poi.POIDataSamples; +import org.apache.poi.hmef.HMEFMessage; +import org.apache.poi.hmef.attribute.TNEFProperty; +import org.apache.poi.util.LittleEndian; import org.apache.poi.util.TempFile; +import org.apache.poi.util.StringUtil; import org.junit.jupiter.api.Test; public class TestHMEFContentsExtractor { @@ -81,4 +89,43 @@ void testExtractMessageBody_File() throws IOException { extractor.extractMessageBody(rtf); assertTrue(rtf.length() > 0, "RTF message body is empty"); } + + @Test + void testExtractAttachmentsRejectsPathTraversal() throws IOException { + File outputDirectory = TempFile.createTempDirectory("hmef-attachments"); + File escapedFile = new File( + outputDirectory.getParentFile(), outputDirectory.getName() + "-escaped.txt"); + if (escapedFile.exists()) { + assertTrue(escapedFile.delete()); + } + assertFalse(escapedFile.exists()); + + HMEFContentsExtractor extractor = new HMEFContentsExtractor( + new HMEFMessage(new ByteArrayInputStream(createTnefWithAttachment( + ".." + File.separator + escapedFile.getName(), "contents")))); + + assertThrows(IOException.class, () -> extractor.extractAttachments(outputDirectory)); + assertFalse(escapedFile.exists()); + assertTrue(outputDirectory.delete()); + } + + private static byte[] createTnefWithAttachment(String filename, String contents) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + LittleEndian.putInt(HMEFMessage.HEADER_SIGNATURE, out); + LittleEndian.putUShort(0, out); + writeAttribute(out, TNEFProperty.ID_ATTACHRENDERDATA.id, TNEFProperty.TYPE_BYTE, new byte[0]); + writeAttribute(out, TNEFProperty.ID_ATTACHTITLE.id, TNEFProperty.TYPE_STRING, + (filename + "\0").getBytes(StringUtil.UTF8)); + writeAttribute(out, TNEFProperty.ID_ATTACHDATA.id, TNEFProperty.TYPE_BYTE, contents.getBytes(StringUtil.UTF8)); + return out.toByteArray(); + } + + private static void writeAttribute(ByteArrayOutputStream out, int id, int type, byte[] data) throws IOException { + out.write(TNEFProperty.LEVEL_ATTACHMENT); + LittleEndian.putUShort(id, out); + LittleEndian.putUShort(type, out); + LittleEndian.putInt(data.length, out); + out.write(data, 0, data.length); + LittleEndian.putUShort(0, out); + } }