Skip to content

Commit c03fdff

Browse files
author
Maruan Sahyoun
committed
PDFBOX-6185: add creation of temp files; code review and tests by Claude Sonnet
git-svn-id: https://svn.apache.org/repos/asf/pdfbox/trunk@1932922 13f79535-47bb-0310-9956-ffa450edef68
1 parent 8e41dba commit c03fdff

2 files changed

Lines changed: 363 additions & 42 deletions

File tree

io/src/main/java/org/apache/pdfbox/io/IOUtils.java

Lines changed: 166 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,21 @@
4545
import java.nio.file.attribute.AclEntryPermission;
4646
import java.nio.file.attribute.AclEntryType;
4747
import java.nio.file.attribute.AclFileAttributeView;
48+
import java.nio.file.attribute.FileAttribute;
49+
import java.nio.file.attribute.PosixFilePermission;
4850
import java.nio.file.attribute.PosixFilePermissions;
4951
import java.nio.file.attribute.UserPrincipal;
5052
import java.security.AccessController;
5153
import java.security.PrivilegedAction;
54+
import java.util.ArrayList;
5255
import java.util.Collections;
5356
import java.util.Comparator;
57+
import java.util.HashSet;
58+
import java.util.List;
5459
import java.util.Objects;
5560
import java.util.Optional;
61+
import java.util.Set;
62+
import java.util.concurrent.atomic.AtomicBoolean;
5663
import java.util.function.Consumer;
5764
import 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

Comments
 (0)