4545import java .nio .file .attribute .AclEntryPermission ;
4646import java .nio .file .attribute .AclEntryType ;
4747import java .nio .file .attribute .AclFileAttributeView ;
48+ import java .nio .file .attribute .FileAttribute ;
49+ import java .nio .file .attribute .PosixFilePermission ;
4850import java .nio .file .attribute .PosixFilePermissions ;
4951import java .nio .file .attribute .UserPrincipal ;
5052import java .security .AccessController ;
5153import java .security .PrivilegedAction ;
54+ import java .util .ArrayList ;
5255import java .util .Collections ;
5356import java .util .Comparator ;
57+ import java .util .HashSet ;
58+ import java .util .List ;
5459import java .util .Objects ;
5560import java .util .Optional ;
61+ import java .util .Set ;
62+ import java .util .concurrent .atomic .AtomicBoolean ;
5663import java .util .function .Consumer ;
5764import java .util .stream .Stream ;
5865
@@ -75,6 +82,22 @@ public final class IOUtils
7582 //TODO PDFBox should really use Apache Commons IO.
7683 private static final Optional <Consumer <ByteBuffer >> UNMAPPER ;
7784
85+ // POSIX file permissions for temporary files and directories (owner read/write/execute only)
86+ private static final Set <PosixFilePermission > POSIX_DIR_PERMS =
87+ PosixFilePermissions .fromString ("rwx------" );
88+ private static final Set <PosixFilePermission > POSIX_FILE_PERMS =
89+ PosixFilePermissions .fromString ("rw-------" );
90+
91+ // Derived FileAttribute wrappers for creation-time use
92+ private static final FileAttribute <Set <PosixFilePermission >> POSIX_DIR_PERMISSIONS =
93+ PosixFilePermissions .asFileAttribute (POSIX_DIR_PERMS );
94+ private static final FileAttribute <Set <PosixFilePermission >> POSIX_FILE_PERMISSIONS =
95+ PosixFilePermissions .asFileAttribute (POSIX_FILE_PERMS );
96+
97+ private static final List <Path > TEMP_DIRS_TO_DELETE = Collections .synchronizedList (new ArrayList <>());
98+ private static final AtomicBoolean SHUTDOWN_HOOK_REGISTERED = new AtomicBoolean (false );
99+
100+
78101 static
79102 {
80103 UNMAPPER = Optional .ofNullable (AccessController
@@ -345,80 +368,181 @@ public static StreamCacheCreateFunction createTempFileOnlyStreamCache()
345368 * other users or processes on the same system. Used e.g. by PDFDebugger.</p>
346369 *
347370 * @return the path to the created temporary directory
348- * @throws IOException
371+ * @throws IOException if an I/O error occurs during directory creation or permission setting
349372 */
350373 public static Path createProtectedTempDir () throws IOException
351374 {
352- // S5443: permissions are immediately restricted to owner-only by
353- // applyOwnerOnlyPermissions(), mitigating the default-permission risk.
354- @ SuppressWarnings ("java:S5443" )
355- Path tempPath = Files .createTempDirectory ("pdfbox-" );
356- applyOwnerOnlyPermissions (tempPath );
375+ Path tempPath ;
376+ // Set owner-only permissions at file creation time if possible, to minimize the time window where
377+ // the file has default permissions.
378+ if (FileSystems .getDefault ().supportedFileAttributeViews ().contains ("posix" ))
379+ {
380+ tempPath = Files .createTempDirectory ("pdfbox-" , POSIX_DIR_PERMISSIONS );
381+ }
382+ else
383+ {
384+ // S5443: permissions are immediately restricted to owner-only by
385+ // applyOwnerOnlyPermissions(), mitigating the default-permission risk.
386+ @ SuppressWarnings ("java:S5443" )
387+ Path p = Files .createTempDirectory ("pdfbox-" );
388+ tempPath = p ;
389+ applyOwnerOnlyPermissions (tempPath , true );
390+ }
391+
392+ registerForDeletion (tempPath );
357393
394+ return tempPath ;
395+ }
396+
397+ private static void registerForDeletion (Path path ) {
398+ TEMP_DIRS_TO_DELETE .add (path );
358399 // use shutdown hook instead of deleteOnExit() to ensure deletion
359400 // of the entire directory in case of not automatically deleted on
360401 // JVM exit (e.g. due to open file handles or when the temp directory is not empty)
361- Runtime . getRuntime (). addShutdownHook ( new Thread (() ->
402+ if ( SHUTDOWN_HOOK_REGISTERED . compareAndSet ( false , true ))
362403 {
363- try (Stream <Path > entries = Files .walk (tempPath ))
364- {
365- entries .sorted (Comparator .reverseOrder ())
366- .forEach (p -> p .toFile ().delete ());
367- }
368- catch (IOException ignored ) {}
369- }));
404+ Runtime .getRuntime ().addShutdownHook (new Thread (() ->
405+ TEMP_DIRS_TO_DELETE .forEach (IOUtils ::deletePathRecursively )
406+ ));
407+ }
408+ }
370409
371- return tempPath ;
410+ private static void deletePathRecursively (Path path ) {
411+ try (Stream <Path > entries = Files .walk (path ))
412+ {
413+ entries .sorted (Comparator .reverseOrder ())
414+ // we are using File.delete() on purpose over Files.deleteIfExists() which would be prefered in general,
415+ // as it's throwing a checked exception. As we are doing that in a shutdown hook there is not much we can
416+ // do about it and a logger might no longer be available.
417+ .forEach (p -> p .toFile ().delete ());
418+ }
419+ catch (IOException ignored ) {}
372420 }
373421
374- private static void applyOwnerOnlyPermissions (Path dir ) throws IOException
422+ /**
423+ * Creates a temporary file in the specified directory (or default temporary-file directory
424+ * if null) with owner-only permissions.
425+ *
426+ * <p>This method attempts to set owner-only permissions at file creation time when supported,
427+ * to minimize the time window during which the file may have default (world-readable) permissions.
428+ * On POSIX systems (Linux, macOS), permissions are set during creation. On Windows, permissions
429+ * are set after file creation via ACL.</p>
430+ *
431+ * <p>Note: This method is designed for storing temporary files that may contain sensitive data
432+ * in a temporary directory with restricted permissions, to mitigate the risk of unauthorized
433+ * access by other users or processes on the same system. However, unlike {@link #createProtectedTempDir()},
434+ * this method does NOT automatically delete the file on JVM shutdown. The caller is responsible
435+ * for deleting the temporary file when no longer needed. Used e.g. by PDFDebugger.</p>
436+ *
437+ * @param dir the directory in which to create the temporary file, or null to use the default
438+ * temporary-file directory
439+ * @param prefix the prefix string to be used in generating the file's name; may be null
440+ * @param suffix the suffix string to be used in generating the file's name; may be null
441+ * @return the path to the created temporary file with owner-only permissions
442+ * @throws IOException if an I/O error occurs during file creation or permission setting
443+ * @throws SecurityException if a security manager is installed and denies access
444+ * @see #createProtectedTempDir()
445+ * @see Files#createTempFile(Path, String, String, FileAttribute[])
446+ */
447+ public static Path createProtectedTempFile (Path dir , String prefix , String suffix ) throws IOException
375448 {
449+ // Set owner-only permissions at file creation time if possible, to minimize the time window where
450+ // the file has default permissions.
376451 if (FileSystems .getDefault ().supportedFileAttributeViews ().contains ("posix" ))
377452 {
378- // Unix/macOS — rwx------
379- Files .setPosixFilePermissions (dir , PosixFilePermissions .fromString ("rwx------" ));
453+ return dir == null
454+ ? Files .createTempFile (prefix , suffix , POSIX_FILE_PERMISSIONS )
455+ : Files .createTempFile (dir , prefix , suffix , POSIX_FILE_PERMISSIONS );
456+ }
457+ Path tempFile = dir == null
458+ ? Files .createTempFile (prefix , suffix )
459+ : Files .createTempFile (dir , prefix , suffix );
460+ applyOwnerOnlyPermissions (tempFile , false );
461+ return tempFile ;
462+ }
463+
464+ /**
465+ * Applies owner-only permissions to a file or directory in a platform-specific manner.
466+ *
467+ * <p>This method ensures that the specified file or directory is readable and writable only by its owner,
468+ * with no permissions granted to group or others. The implementation differs based on the underlying filesystem:</p>
469+ *
470+ * <ul>
471+ * <li><b>POSIX systems (Linux, macOS, Unix):</b> Sets permissions to {@code rwx------} for directories
472+ * or {@code rw-------} for files using POSIX file attributes.</li>
473+ * <li><b>Windows systems:</b> Replaces the entire ACL with a single owner-only ALLOW entry granting full control.
474+ * If ACL is not supported, falls back to using {@link File#setReadable(boolean, boolean)},
475+ * {@link File#setWritable(boolean, boolean)}, and {@link File#setExecutable(boolean, boolean)}.</li>
476+ * </ul>
477+ *
478+ * <p>If permissions cannot be set successfully on Windows systems, a warning is logged but no exception is thrown.</p>
479+ *
480+ * @param path the file or directory to apply owner-only permissions to
481+ * @param isDirectory {@code true} if the path is a directory and should have execute permissions;
482+ * {@code false} if it is a file
483+ * @throws IOException if an I/O error occurs while setting POSIX permissions or accessing the file
484+ * @throws SecurityException if a security manager is installed and denies access to the file
485+ * @see Files#setPosixFilePermissions(Path, Set)
486+ * @see Files#getFileAttributeView(Path, Class)
487+ */
488+ private static void applyOwnerOnlyPermissions (Path path , boolean isDirectory ) throws IOException
489+ {
490+ if (FileSystems .getDefault ().supportedFileAttributeViews ().contains ("posix" ))
491+ {
492+ Set <PosixFilePermission > permissions = isDirectory ? POSIX_DIR_PERMS : POSIX_FILE_PERMS ;
493+ Files .setPosixFilePermissions (path , permissions );
380494 }
381495 else
382496 {
383497 // Windows — replace the entire ACL with a single owner-only ALLOW entry
384498 AclFileAttributeView aclView =
385- Files .getFileAttributeView (dir , AclFileAttributeView .class );
499+ Files .getFileAttributeView (path , AclFileAttributeView .class );
386500
387501 if (aclView == null )
388502 {
389- File tempDir = dir .toFile ();
390- boolean isReadable = tempDir .setReadable (true , true );
391- boolean isWritable = tempDir .setWritable (true , true );
392- boolean isExecutable = tempDir .setExecutable (true , true );
393- if (!isReadable || !isWritable || !isExecutable )
503+ File pathAsFile = path .toFile ();
504+ boolean isReadable = pathAsFile .setReadable (true , true );
505+ boolean isWritable = pathAsFile .setWritable (true , true );
506+ boolean isProtected = isReadable && isWritable ;
507+ if (isDirectory )
508+ {
509+ isProtected &= pathAsFile .setExecutable (true , true );
510+ }
511+ if (!isProtected )
394512 {
395- LOG .warn ("Unable to set owner-only permissions on temporary directory : {}. " +
396- "Please ensure that the temporary directory is protected against unauthorized access." ,
397- dir );
513+ LOG .warn ("Unable to set owner-only permissions on: {}. " +
514+ "Please ensure that the file or directory is protected against unauthorized access." ,
515+ path );
398516 }
399517 return ;
400518 }
401519
402520 UserPrincipal owner = aclView .getOwner ();
403521
522+ Set <AclEntryPermission > aclPermissions = new HashSet <>(Set .of (
523+ AclEntryPermission .READ_DATA ,
524+ AclEntryPermission .WRITE_DATA ,
525+ AclEntryPermission .APPEND_DATA ,
526+ AclEntryPermission .READ_NAMED_ATTRS ,
527+ AclEntryPermission .WRITE_NAMED_ATTRS ,
528+ AclEntryPermission .READ_ATTRIBUTES ,
529+ AclEntryPermission .WRITE_ATTRIBUTES ,
530+ AclEntryPermission .DELETE ,
531+ AclEntryPermission .READ_ACL ,
532+ AclEntryPermission .WRITE_ACL ,
533+ AclEntryPermission .SYNCHRONIZE
534+ ));
535+
536+ if (isDirectory )
537+ {
538+ aclPermissions .add (AclEntryPermission .EXECUTE );
539+ aclPermissions .add (AclEntryPermission .DELETE_CHILD );
540+ }
541+
404542 AclEntry ownerFullControl = AclEntry .newBuilder ()
405543 .setType (AclEntryType .ALLOW )
406544 .setPrincipal (owner )
407- .setPermissions (
408- AclEntryPermission .READ_DATA ,
409- AclEntryPermission .WRITE_DATA ,
410- AclEntryPermission .APPEND_DATA ,
411- AclEntryPermission .READ_NAMED_ATTRS ,
412- AclEntryPermission .WRITE_NAMED_ATTRS ,
413- AclEntryPermission .EXECUTE ,
414- AclEntryPermission .DELETE_CHILD ,
415- AclEntryPermission .READ_ATTRIBUTES ,
416- AclEntryPermission .WRITE_ATTRIBUTES ,
417- AclEntryPermission .DELETE ,
418- AclEntryPermission .READ_ACL ,
419- AclEntryPermission .WRITE_ACL ,
420- AclEntryPermission .SYNCHRONIZE
421- )
545+ .setPermissions (aclPermissions )
422546 .build ();
423547
424548 // Set so that only the owner has permissions, and remove any inherited ACL entries
0 commit comments