This is an automated email from the ASF dual-hosted git repository.

fanningpj pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/poi.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 45a3e16e7e add IOUtils.newFile(parent, path) (#855)
45a3e16e7e is described below

commit 45a3e16e7e9518ea8c7ecc85d91c8d37fd61bb39
Author: PJ Fanning <[email protected]>
AuthorDate: Wed Jul 23 12:43:18 2025 +0100

    add IOUtils.newFile(parent, path) (#855)
    
    * add IOUtils.newFile(parent, path)
    
    * Update IOUtils.java
    
    * Update IOUtils.java
---
 .../java/org/apache/poi/poifs/dev/POIFSDump.java   | 12 +++--
 .../apache/poi/poifs/macros/VBAMacroExtractor.java |  3 +-
 poi/src/main/java/org/apache/poi/util/IOUtils.java | 23 ++++++++-
 .../test/java/org/apache/poi/util/TestIOUtils.java | 54 ++++++++++++++++++++++
 4 files changed, 85 insertions(+), 7 deletions(-)

diff --git a/poi/src/main/java/org/apache/poi/poifs/dev/POIFSDump.java 
b/poi/src/main/java/org/apache/poi/poifs/dev/POIFSDump.java
index d29745ea82..fdf6bb85f4 100644
--- a/poi/src/main/java/org/apache/poi/poifs/dev/POIFSDump.java
+++ b/poi/src/main/java/org/apache/poi/poifs/dev/POIFSDump.java
@@ -71,7 +71,7 @@ public final class POIFSDump {
                 DirectoryEntry root = fs.getRoot();
                 String filenameWithoutPath = new File(filename).getName();
                 File dumpDir = new File(filenameWithoutPath + "_dump");
-                File file = new File(dumpDir, root.getName());
+                File file = IOUtils.newFile(dumpDir, root.getName());
                 if (!file.exists() && !file.mkdirs()) {
                     throw new IOException("Could not create directory " + 
file);
                 }
@@ -104,13 +104,14 @@ public final class POIFSDump {
                 try (DocumentInputStream is = new DocumentInputStream(node)) {
                    bytes = IOUtils.toByteArray(is);
                 }
-                try (OutputStream out = Files.newOutputStream(new File(parent, 
node.getName().trim()).toPath())) {
+                try (OutputStream out = Files.newOutputStream(
+                        IOUtils.newFile(parent, 
node.getName().trim()).toPath())) {
                     out.write(bytes);
                 }
             } else if (entry instanceof DirectoryEntry){
                 DirectoryEntry dir = (DirectoryEntry)entry;
-                File file = new File(parent, entry.getName());
-                if(!file.exists() && !file.mkdirs()) {
+                File file = IOUtils.newFile(parent, entry.getName());
+                if (!file.exists() && !file.mkdirs()) {
                     throw new IOException("Could not create directory " + 
file);
                 }
                 dump(dir, file);
@@ -119,8 +120,9 @@ public final class POIFSDump {
             }
         }
     }
+
     public static void dump(POIFSFileSystem fs, int startBlock, String name, 
File parent) throws IOException {
-        File file = new File(parent, name);
+        File file = IOUtils.newFile(parent, name);
         try (OutputStream out = Files.newOutputStream(file.toPath())) {
             POIFSStream stream = new POIFSStream(fs, startBlock);
 
diff --git 
a/poi/src/main/java/org/apache/poi/poifs/macros/VBAMacroExtractor.java 
b/poi/src/main/java/org/apache/poi/poifs/macros/VBAMacroExtractor.java
index c0db47e5fb..e5a80e9034 100644
--- a/poi/src/main/java/org/apache/poi/poifs/macros/VBAMacroExtractor.java
+++ b/poi/src/main/java/org/apache/poi/poifs/macros/VBAMacroExtractor.java
@@ -26,6 +26,7 @@ import java.nio.file.Files;
 import java.util.Map;
 import java.util.Map.Entry;
 
+import org.apache.poi.util.IOUtils;
 import org.apache.poi.util.StringUtil;
 
 /**
@@ -95,7 +96,7 @@ public class VBAMacroExtractor {
                 System.out.println();
                 System.out.println(moduleCode);
             } else {
-                File out = new File(outputDir, moduleName + extension);
+                File out = IOUtils.newFile(outputDir, moduleName + extension);
                 try (OutputStream fout = Files.newOutputStream(out.toPath());
                      OutputStreamWriter fwriter = new OutputStreamWriter(fout, 
StringUtil.UTF8)) {
                     fwriter.write(moduleCode);
diff --git a/poi/src/main/java/org/apache/poi/util/IOUtils.java 
b/poi/src/main/java/org/apache/poi/util/IOUtils.java
index ff86043a54..b913a9bf03 100644
--- a/poi/src/main/java/org/apache/poi/util/IOUtils.java
+++ b/poi/src/main/java/org/apache/poi/util/IOUtils.java
@@ -601,7 +601,6 @@ public final class IOUtils {
         return Arrays.copyOfRange(src, offset, offset+realLength);
     }
 
-
     /**
      * Simple utility function to check that you haven't hit EOF
      * when reading a byte.
@@ -618,6 +617,28 @@ public final class IOUtils {
         return b;
     }
 
+    /**
+     * Creates a new file in the given parent directory with the given name.
+     * There is a check to prevent path traversal attacks. Only path traversal
+     * that would lead to a file outside the parent directory is regarded as 
an issue.
+     *
+     * @param parent The parent directory where the file should be created.
+     * @param name The name of the file to create.
+     * @return The created file.
+     * @throws IOException If path traversal is detected.
+     * @since POI 5.4.2
+     */
+    public static File newFile(final File parent, final String name) throws 
IOException {
+        final File file = new File(parent, name);
+        if (!file.toPath().toAbsolutePath().normalize().startsWith(
+                parent.toPath().toAbsolutePath().normalize()
+        )) {
+            throw new IOException(String.format(
+                    Locale.ROOT, "Failing due to path traversal in `%s`", 
name));
+        }
+        return file;
+    }
+
     private static void throwRFE(long length, int maxLength) {
         throw new RecordFormatException(String.format(Locale.ROOT, "Tried to 
allocate an array of length %,d" +
                         ", but the maximum length for this record type is 
%,d.%n" +
diff --git a/poi/src/test/java/org/apache/poi/util/TestIOUtils.java 
b/poi/src/test/java/org/apache/poi/util/TestIOUtils.java
index 7f026adc74..094e138eef 100644
--- a/poi/src/test/java/org/apache/poi/util/TestIOUtils.java
+++ b/poi/src/test/java/org/apache/poi/util/TestIOUtils.java
@@ -41,6 +41,7 @@ import java.nio.charset.StandardCharsets;
 import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
 import org.apache.poi.EmptyFileException;
 import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.parallel.Isolated;
@@ -575,6 +576,59 @@ final class TestIOUtils {
         assertEquals(4, ret.length);
     }
 
+    @Test
+    void testNewFile() throws  IOException {
+        final File parent = TempFile.createTempDirectory("create-file-test");
+        try {
+            final String path0 = "path/to/file.txt";
+            final File outFile = IOUtils.newFile(parent, path0);
+            assertTrue(outFile.getAbsolutePath().endsWith(path0),
+                    "unexpected path: " + outFile.getAbsolutePath());
+        } finally {
+            assertTrue(parent.delete());
+        }
+    }
+
+    @Test
+    void testAllowedPathTraversal() throws IOException {
+        final File parent = 
TempFile.createTempDirectory("path-traversal-test");
+        try {
+            // this path is ok because it doesn't walk out of the parent 
directory
+            final String path0 = "a/b/c/../d/e/../../f/g/./h";
+            File outFile = IOUtils.newFile(parent, path0);
+            assertTrue(outFile.getAbsolutePath().endsWith(path0),
+                    "unexpected path: " + outFile.getAbsolutePath());
+        } finally {
+            assertTrue(parent.delete());
+        }
+    }
+
+    @Test
+    void testAllowedPathTraversal2() throws IOException {
+        final File parent = 
TempFile.createTempDirectory("path-traversal-test");
+        try {
+            // this path is ok because it doesn't walk out of the parent 
directory
+            // the initial slash is ignored and the generated path is relative 
to the parent directory
+            final String path0 = "/a/b/c.txt";
+            File outFile = IOUtils.newFile(parent, path0);
+            assertTrue(outFile.getAbsolutePath().endsWith(path0),
+                    "unexpected path: " + outFile.getAbsolutePath());
+        } finally {
+            assertTrue(parent.delete());
+        }
+    }
+
+    @Test
+    void testDisallowedPathTraversal() throws  IOException {
+        final File parent = 
TempFile.createTempDirectory("path-traversal-test");
+        try {
+            final String path0 = "../a/b/c.txt";
+            Assertions.assertThrows(IOException.class, () -> 
IOUtils.newFile(parent, path0));
+        } finally {
+            assertTrue(parent.delete());
+        }
+    }
+
     /**
      * This returns 0 for the first call to skip and then reads
      * as requested.  This tests that the fallback to read() works.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to