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);
+ }
+ }
+ }
}