Author: msahyoun
Date: Thu Apr  9 17:26:30 2026
New Revision: 1932930

Log:
PDFBOX-6185: backport temp file handling from trunk

Modified:
   pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/IOUtils.java
   pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/ScratchFile.java
   pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/TestIOUtils.java

Modified: pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/IOUtils.java
==============================================================================
--- pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/IOUtils.java      
Thu Apr  9 16:44:16 2026        (r1932929)
+++ pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/IOUtils.java      
Thu Apr  9 17:26:30 2026        (r1932930)
@@ -46,14 +46,22 @@ import java.nio.file.attribute.AclEntry;
 import java.nio.file.attribute.AclEntryPermission;
 import java.nio.file.attribute.AclEntryType;
 import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermissions;
 import java.nio.file.attribute.UserPrincipal;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
@@ -76,6 +84,21 @@ public final class IOUtils
     //TODO PDFBox should really use Apache Commons IO.
     private static final Optional<Consumer<ByteBuffer>> UNMAPPER;
 
+    // POSIX file permissions for temporary files and directories (owner 
read/write/execute only)
+    private static final Set<PosixFilePermission> POSIX_DIR_PERMS =
+        PosixFilePermissions.fromString("rwx------");
+    private static final Set<PosixFilePermission> POSIX_FILE_PERMS =
+        PosixFilePermissions.fromString("rw-------");
+
+    // Derived FileAttribute wrappers for creation-time use
+    private static final FileAttribute<Set<PosixFilePermission>> 
POSIX_DIR_PERMISSIONS =
+        PosixFilePermissions.asFileAttribute(POSIX_DIR_PERMS);
+    private static final FileAttribute<Set<PosixFilePermission>> 
POSIX_FILE_PERMISSIONS =
+        PosixFilePermissions.asFileAttribute(POSIX_FILE_PERMS);
+
+    private static final List<Path> TEMP_DIRS_TO_DELETE = 
Collections.synchronizedList(new ArrayList<>());
+    private static final AtomicBoolean SHUTDOWN_HOOK_REGISTERED = new 
AtomicBoolean(false);
+
     static
     {
         UNMAPPER = Optional.ofNullable(AccessController
@@ -365,75 +388,179 @@ public final class IOUtils
      */
     public static Path createProtectedTempDir() throws IOException
     {
-        // S5443: permissions are immediately restricted to owner-only by
-        // applyOwnerOnlyPermissions(), mitigating the default-permission risk.
-        @SuppressWarnings("java:S5443")
-        Path tempPath = Files.createTempDirectory("pdfbox-");
-        applyOwnerOnlyPermissions(tempPath);
+        Path tempPath;
+        // Set owner-only permissions at file creation time if possible, to 
minimize the time window where
+        // the file has default permissions.
+        if 
(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"))
+        {
+            tempPath = Files.createTempDirectory("pdfbox-", 
POSIX_DIR_PERMISSIONS);
+        }
+        else
+        {
+            // S5443: permissions are immediately restricted to owner-only by
+            // applyOwnerOnlyPermissions(), mitigating the default-permission 
risk.
+            @SuppressWarnings("java:S5443")
+            Path p = Files.createTempDirectory("pdfbox-");
+            tempPath = p;
+            applyOwnerOnlyPermissions(tempPath, true);
+        }
+
+        registerForDeletion(tempPath);
 
+        return tempPath;
+    }
+
+    private static void registerForDeletion(Path path) {
+        TEMP_DIRS_TO_DELETE.add(path);
         // use shutdown hook instead of deleteOnExit() to ensure deletion
         // of the entire directory in case of not automatically deleted on 
         // JVM exit (e.g. due to open file handles or when the temp directory 
is not empty)
-        Runtime.getRuntime().addShutdownHook(new Thread(() ->
+        if (SHUTDOWN_HOOK_REGISTERED.compareAndSet(false, true))
         {
-            try (Stream<Path> entries = Files.walk(tempPath))
-            {
-                entries.sorted(Comparator.reverseOrder())
-                    .forEach(p -> p.toFile().delete());
-            }
-            catch (IOException ignored) {}
-        }));
+            Runtime.getRuntime().addShutdownHook(new Thread(() ->
+                TEMP_DIRS_TO_DELETE.forEach(IOUtils::deletePathRecursively)
+            ));
+        }
+    }
 
-        return tempPath;
+    private static void deletePathRecursively(Path path) {
+        try (Stream<Path> entries = Files.walk(path))
+        {
+            entries.sorted(Comparator.reverseOrder())
+                // we are using File.delete() on purpose over 
Files.deleteIfExists() which would be prefered in general, 
+                // as it's throwing a checked exception. As we are doing that 
in a shutdown hook there is not much we can
+                // do about it and a logger might no longer be available.
+                .forEach(p -> p.toFile().delete());
+        }
+        catch (IOException ignored) {}
+    }
+
+    /**
+     * Creates a temporary file in the specified directory (or default 
temporary-file directory 
+     * if null) with owner-only permissions.
+     * 
+     * <p>This method attempts to set owner-only permissions at file creation 
time when supported,
+     * to minimize the time window during which the file may have default 
(world-readable) permissions.
+     * On POSIX systems (Linux, macOS), permissions are set during creation. 
On Windows, permissions
+     * are set after file creation via ACL.</p>
+     * 
+     * <p>Note: This method is designed for storing temporary files that may 
contain sensitive data
+     * in a temporary directory with restricted permissions, to mitigate the 
risk of unauthorized 
+     * access by other users or processes on the same system. However, unlike 
{@link #createProtectedTempDir()},
+     * this method does NOT automatically delete the file on JVM shutdown. The 
caller is responsible 
+     * for deleting the temporary file when no longer needed. Used e.g. by 
PDFDebugger.</p>
+     * 
+     * @param dir the directory in which to create the temporary file, or null 
to use the default 
+     *            temporary-file directory
+     * @param prefix the prefix string to be used in generating the file's 
name; may be null
+     * @param suffix the suffix string to be used in generating the file's 
name; may be null
+     * @return the path to the created temporary file with owner-only 
permissions
+     * @throws IOException if an I/O error occurs during file creation or 
permission setting
+     * @throws SecurityException if a security manager is installed and denies 
access
+     * @see #createProtectedTempDir()
+     * @see Files#createTempFile(Path, String, String, FileAttribute[])
+     */
+    public static Path createProtectedTempFile(Path dir, String prefix, String 
suffix) throws IOException
+    {
+        // Set owner-only permissions at file creation time if possible, to 
minimize the time window where
+        // the file has default permissions.
+        if 
(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"))
+        {
+            return dir == null 
+                ? Files.createTempFile(prefix, suffix, POSIX_FILE_PERMISSIONS) 
+                : Files.createTempFile(dir, prefix, suffix, 
POSIX_FILE_PERMISSIONS);
+        }            
+        // S5443: permissions are immediately restricted to owner-only by
+        // applyOwnerOnlyPermissions(), mitigating the default-permission risk.
+        @SuppressWarnings("java:S5443")
+        Path tempFile = dir == null 
+            ? Files.createTempFile(prefix, suffix) 
+            : Files.createTempFile(dir, prefix, suffix);
+        applyOwnerOnlyPermissions(tempFile, false);
+        return tempFile;
     }
 
-    private static void applyOwnerOnlyPermissions(Path dir) throws IOException
+    /**
+     * Applies owner-only permissions to a file or directory in a 
platform-specific manner.
+     * 
+     * <p>This method ensures that the specified file or directory is readable 
and writable only by its owner,
+     * with no permissions granted to group or others. The implementation 
differs based on the underlying filesystem:</p>
+     * 
+     * <ul>
+     *   <li><b>POSIX systems (Linux, macOS, Unix):</b> Sets permissions to 
{@code rwx------} for directories
+     *       or {@code rw-------} for files using POSIX file attributes.</li>
+     *   <li><b>Windows systems:</b> Replaces the entire ACL with a single 
owner-only ALLOW entry granting full control.
+     *       If ACL is not supported, falls back to using {@link 
File#setReadable(boolean, boolean)},
+     *       {@link File#setWritable(boolean, boolean)}, and {@link 
File#setExecutable(boolean, boolean)}.</li>
+     * </ul>
+     * 
+     * <p>If permissions cannot be set successfully on Windows systems, a 
warning is logged but no exception is thrown.</p>
+     * 
+     * @param path the file or directory to apply owner-only permissions to
+     * @param isDirectory {@code true} if the path is a directory and should 
have execute permissions;
+     *                    {@code false} if it is a file
+     * @throws IOException if an I/O error occurs while setting POSIX 
permissions or accessing the file
+     * @throws SecurityException if a security manager is installed and denies 
access to the file
+     * @see Files#setPosixFilePermissions(Path, Set)
+     * @see Files#getFileAttributeView(Path, Class)
+     */
+    private static void applyOwnerOnlyPermissions(Path path, boolean 
isDirectory) throws IOException
     {
         if 
(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"))
         {
-            // Unix/macOS — rwx------
-            Files.setPosixFilePermissions(dir, 
PosixFilePermissions.fromString("rwx------"));
+            Set<PosixFilePermission> permissions = isDirectory ? 
POSIX_DIR_PERMS : POSIX_FILE_PERMS;
+            Files.setPosixFilePermissions(path, permissions);
         }
         else
         {
             // Windows — replace the entire ACL with a single owner-only ALLOW 
entry
             AclFileAttributeView aclView =
-            Files.getFileAttributeView(dir, AclFileAttributeView.class);
+            Files.getFileAttributeView(path, AclFileAttributeView.class);
 
             if (aclView == null)
             {
-                File tempDir = dir.toFile();
-                boolean isReadable = tempDir.setReadable(true, true);
-                boolean isWritable = tempDir.setWritable(true, true);
-                boolean isExecutable = tempDir.setExecutable(true, true);
-                if (!isReadable || !isWritable || !isExecutable)
+                File pathAsFile = path.toFile();
+                boolean isReadable = pathAsFile.setReadable(true, true);
+                boolean isWritable = pathAsFile.setWritable(true, true);
+                boolean isProtected = isReadable && isWritable;
+                if (isDirectory)
                 {
-                    LOG.warn("Unable to set owner-only permissions on 
temporary directory: " + dir +
-                            ". Please ensure that the temporary directory is 
protected against unauthorized access.");
+                    isProtected &= pathAsFile.setExecutable(true, true);
+                } 
+                if (!isProtected)
+                {
+                    LOG.warn("Unable to set owner-only permissions on: " + 
path + ". " +
+                            "Please ensure that the file or directory is 
protected against unauthorized access.");
                 }
                 return;
             }
 
             UserPrincipal owner = aclView.getOwner();
 
+            Set<AclEntryPermission> aclPermissions = new 
HashSet<>(Arrays.asList(
+                AclEntryPermission.READ_DATA,
+                AclEntryPermission.WRITE_DATA,
+                AclEntryPermission.APPEND_DATA,
+                AclEntryPermission.READ_NAMED_ATTRS,
+                AclEntryPermission.WRITE_NAMED_ATTRS,
+                AclEntryPermission.READ_ATTRIBUTES,
+                AclEntryPermission.WRITE_ATTRIBUTES,
+                AclEntryPermission.DELETE,
+                AclEntryPermission.READ_ACL,
+                AclEntryPermission.WRITE_ACL,
+                AclEntryPermission.SYNCHRONIZE
+            ));
+
+            if (isDirectory)
+            {
+                aclPermissions.add(AclEntryPermission.EXECUTE);
+                aclPermissions.add(AclEntryPermission.DELETE_CHILD);
+            }
+
             AclEntry ownerFullControl = AclEntry.newBuilder()
                 .setType(AclEntryType.ALLOW)
                 .setPrincipal(owner)
-                .setPermissions(
-                    AclEntryPermission.READ_DATA,
-                    AclEntryPermission.WRITE_DATA,
-                    AclEntryPermission.APPEND_DATA,
-                    AclEntryPermission.READ_NAMED_ATTRS,
-                    AclEntryPermission.WRITE_NAMED_ATTRS,
-                    AclEntryPermission.EXECUTE,
-                    AclEntryPermission.DELETE_CHILD,
-                    AclEntryPermission.READ_ATTRIBUTES,
-                    AclEntryPermission.WRITE_ATTRIBUTES,
-                    AclEntryPermission.DELETE,
-                    AclEntryPermission.READ_ACL,
-                    AclEntryPermission.WRITE_ACL,
-                    AclEntryPermission.SYNCHRONIZE
-                )
+                .setPermissions(aclPermissions)
                 .build();
 
             // Set so that only the owner has permissions, and remove any 
inherited ACL entries

Modified: 
pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/ScratchFile.java
==============================================================================
--- pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/ScratchFile.java  
Thu Apr  9 16:44:16 2026        (r1932929)
+++ pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/ScratchFile.java  
Thu Apr  9 17:26:30 2026        (r1932930)
@@ -19,7 +19,6 @@ package org.apache.pdfbox.io;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.List;
@@ -252,11 +251,11 @@ public class ScratchFile implements Rand
                 {
                     if (scratchFileDirectory == null)
                     {
-                        file = Files.createTempFile("PDFBox", ".tmp").toFile();
+                        file = IOUtils.createProtectedTempFile(null, "PDFBox", 
".tmp").toFile();
                     }
                     else
                     {
-                        file = 
Files.createTempFile(scratchFileDirectory.toPath(), "PDFBox", ".tmp").toFile();
+                        file = 
IOUtils.createProtectedTempFile(scratchFileDirectory.toPath(), "PDFBox", 
".tmp").toFile();
                     }
                     try
                     {

Modified: 
pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/TestIOUtils.java
==============================================================================
--- pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/TestIOUtils.java  
Thu Apr  9 16:44:16 2026        (r1932929)
+++ pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/TestIOUtils.java  
Thu Apr  9 17:26:30 2026        (r1932930)
@@ -408,4 +408,201 @@ class TestIOUtils
             }
         }
     }
+
+    /**
+     * Tests {@link IOUtils#createProtectedTempFile(Path, String, String)} 
+     * creates a file in the default temporary-file directory.
+     * @throws IOException if an I/O error occurs
+     */
+    @Test
+    void testCreateProtectedTempFileDefaultDir() throws IOException
+    {
+        Path tempFile = IOUtils.createProtectedTempFile(null, "test", ".tmp");
+        try
+        {
+            assertTrue(Files.exists(tempFile), "Temporary file should exist");
+            assertTrue(Files.isRegularFile(tempFile), "Path should be a file");
+            assertTrue(tempFile.getFileName().toString().startsWith("test"), 
+                    "File name should start with 'test'");
+            assertTrue(tempFile.getFileName().toString().endsWith(".tmp"), 
+                    "File name should end with '.tmp'");
+        }
+        finally
+        {
+            // Cleanup
+            if (Files.exists(tempFile))
+            {
+                Files.delete(tempFile);
+            }
+        }
+    }
+
+    /**
+     * Tests {@link IOUtils#createProtectedTempFile(Path, String, String)} 
+     * creates a file in a specified directory.
+     * @throws IOException if an I/O error occurs
+     */
+    @Test
+    void testCreateProtectedTempFileSpecifiedDir() throws IOException
+    {
+        Path tempDir = IOUtils.createProtectedTempDir();
+        try
+        {
+            Path tempFile = IOUtils.createProtectedTempFile(tempDir, "myfile", 
".bin");
+            try
+            {
+                assertTrue(Files.exists(tempFile), "Temporary file should 
exist");
+                assertTrue(Files.isRegularFile(tempFile), "Path should be a 
file");
+                assertEquals(tempDir, tempFile.getParent(), "File should be in 
specified directory");
+                
assertTrue(tempFile.getFileName().toString().startsWith("myfile"), 
+                        "File name should start with 'myfile'");
+                assertTrue(tempFile.getFileName().toString().endsWith(".bin"), 
+                        "File name should end with '.bin'");
+            }
+            finally
+            {
+                // Cleanup temp file
+                if (Files.exists(tempFile))
+                {
+                    Files.delete(tempFile);
+                }
+            }
+        }
+        finally
+        {
+            // Cleanup temp directory
+            if (Files.exists(tempDir))
+            {
+                Files.delete(tempDir);
+            }
+        }
+    }
+
+    /**
+     * Tests {@link IOUtils#createProtectedTempFile(Path, String, String)} 
+     * with POSIX permissions.
+     * @throws IOException if an I/O error occurs
+     */
+    @Test
+    void testCreateProtectedTempFilePermissions() throws IOException
+    {
+        Path tempFile = IOUtils.createProtectedTempFile(null, "perm", ".test");
+        try
+        {
+            // Check if system supports POSIX permissions
+            if 
(Files.getFileStore(tempFile).supportsFileAttributeView("posix"))
+            {
+                Set<PosixFilePermission> perms = 
Files.getPosixFilePermissions(tempFile);
+                
+                // Should have owner read and write
+                assertTrue(perms.contains(PosixFilePermission.OWNER_READ));
+                assertTrue(perms.contains(PosixFilePermission.OWNER_WRITE));
+                
+                // Should NOT have owner execute for files
+                assertFalse(perms.contains(PosixFilePermission.OWNER_EXECUTE));
+                
+                // Should NOT have group or others permissions
+                assertFalse(perms.contains(PosixFilePermission.GROUP_READ));
+                assertFalse(perms.contains(PosixFilePermission.GROUP_WRITE));
+                assertFalse(perms.contains(PosixFilePermission.GROUP_EXECUTE));
+                assertFalse(perms.contains(PosixFilePermission.OTHERS_READ));
+                assertFalse(perms.contains(PosixFilePermission.OTHERS_WRITE));
+                
assertFalse(perms.contains(PosixFilePermission.OTHERS_EXECUTE));
+            }
+        }
+        finally
+        {
+            // Cleanup
+            if (Files.exists(tempFile))
+            {
+                Files.delete(tempFile);
+            }
+        }
+    }
+
+    /**
+     * Tests {@link IOUtils#createProtectedTempFile(Path, String, String)} 
+     * creates multiple unique files.
+     * @throws IOException if an I/O error occurs
+     */
+    @Test
+    void testCreateProtectedTempFileMultiple() throws IOException
+    {
+        Path tempFile1 = IOUtils.createProtectedTempFile(null, "test1", 
".tmp");
+        Path tempFile2 = IOUtils.createProtectedTempFile(null, "test1", 
".tmp");
+        
+        try
+        {
+            assertTrue(Files.exists(tempFile1));
+            assertTrue(Files.exists(tempFile2));
+            // Paths should be different (unique files)
+            assertFalse(tempFile1.equals(tempFile2));
+        }
+        finally
+        {
+            // Cleanup
+            if (Files.exists(tempFile1))
+            {
+                Files.delete(tempFile1);
+            }
+            if (Files.exists(tempFile2))
+            {
+                Files.delete(tempFile2);
+            }
+        }
+    }
+
+    /**
+     * Tests {@link IOUtils#createProtectedTempFile(Path, String, String)} 
+     * with null suffix.
+     * @throws IOException if an I/O error occurs
+     */
+    @Test
+    void testCreateProtectedTempFileNullSuffix() throws IOException
+    {
+        Path tempFile = IOUtils.createProtectedTempFile(null, "test", null);
+        try
+        {
+            assertTrue(Files.exists(tempFile), "Temporary file should exist");
+            assertTrue(Files.isRegularFile(tempFile), "Path should be a file");
+        }
+        finally
+        {
+            // Cleanup
+            if (Files.exists(tempFile))
+            {
+                Files.delete(tempFile);
+            }
+        }
+    }
+
+    /**
+     * Tests {@link IOUtils#createProtectedTempFile(Path, String, String)} 
+     * can create and write to the file.
+     * @throws IOException if an I/O error occurs
+     */
+    @Test
+    void testCreateProtectedTempFileWriteable() throws IOException
+    {
+        Path tempFile = IOUtils.createProtectedTempFile(null, "writable", 
".dat");
+        try
+        {
+            // Write some test data
+            byte[] testData = "Test content".getBytes();
+            Files.write(tempFile, testData);
+            
+            // Read back and verify
+            byte[] readData = Files.readAllBytes(tempFile);
+            assertEquals(testData.length, readData.length);
+            assertEquals("Test content", new String(readData));
+        }
+        finally
+        {
+            // Cleanup
+            if (Files.exists(tempFile))
+            {
+                Files.delete(tempFile);
+            }
+        }
+    }
 }

Reply via email to