Multiuser systems allow multiple users with different privileges to share a file system. Each user, in such an environment, must be able to determine which files are shared and which are private, and each user must be able to enforce these decisions.
Unfortunately, there are a wide variety of file system vulnerabilities that can be exploited by an attacker to gain access to files for which they lack sufficient privileges, particularly when operating on files that reside in shared directories in which multiple users may create, move, or delete files. Privilege escalation is also possible in cases where these programs run with elevated privileges. To prevent these exploits, a program must only operate on files in secure directories.
A directory is secure with respect to a particular user if the user has exclusive privileges to move or delete files inside the directory. Furthermore, each parent to the secure directory can only be be moved or deleted by the user and the system administrator. On most systems, home or user directories are secure by default and only shared directories are insecure.
There are a number of file system properties and capabilities that can be exploited by an attacker, including file links, device files, and shared file access.
File Links
Many operating systems support file links, including symbolic (soft) links, hard links, short cuts, shadows, aliases, and junctions. Symbolic links can be created in POSIX using the ln -s
command and hard links using the ln
command. Hard links are indistinguishable from normal files on POSIX systems.
There are three types of file links supported in the NTFS file system: hard links, junctions, and symbolic links. Symbolic links are available in NTFS starting with Windows Vista.
File links can create security issues for programs that fail to consider the possibility that the file being opened may be a link to a different file. This is especially dangerous when the vulnerable program is running with elevated privileges.
When creating new files, it may be possible to use functions that only create a new file where a file does not already exist. This prevents the application from overwriting an existing file during file creation. Existing Java classes such as FileOutputStream()
and FileWriter()
do not
- allow a programmer to specify that opening a file should fail if the file already exists
- indicate whether an existing file has been opened or a new file has been created
These limitations may lead to a program overwriting or accessing an unintended file.
Device Files
File names on many operating systems may be used to access device files. Reserved MS-DOS device names include AUX
, CON
, PRN
, COM1
, and LPT1
. Character special files and block special files on POSIX systems are used to apply access rights and to direct operations on the files to the appropriate device drivers.
Performing operations on device files that are intended for ordinary character or binary files can result in crashes and denial-of-service attacks. For example, when Windows attempts to interpret the device name as a file resource, it performs an invalid resource access that usually results in a crash [[Howard 2002]].
Device files in UNIX can be a security risk when an attacker can access them in an unauthorized way. For instance, if attackers can read or write to the /dev/kmem
device, they may be able to alter their priority, UID, or other attributes of their process or simply crash the system. Similarly, access to disk devices, tape devices, network devices, and terminals being used by other processes all can lead to problems [[Garfinkel 1996]].
On Linux, it is possible to lock certain applications by attempting to open devices rather than files. Consider the following example:
/dev/mouse /dev/console /dev/tty0 /dev/zero
A Web browser that failed to check for these devices would allow an attacker to create a Web site with image tags such as <IMG src="file:///dev/mouse">
that would lock the user's mouse.
Shared File Access
On many systems, files can be be simultaneously accessed by concurrent processes. Exclusive access grants unrestricted file access to the locking process while denying access to all other processes, eliminating the potential for a race condition on the locked region. The java.nio.channels.FileLock
class facilitates file locking. According to the Java API [[API 2006]] documentation,
A file lock is either exclusive or shared. A shared lock prevents other concurrently-running programs from acquiring an overlapping exclusive lock, but does allow them to acquire overlapping shared locks. An exclusive lock prevents other programs from acquiring an overlapping lock of either type. Once it is released, a lock has no further effect on the locks that may be acquired by other programs.
Shared locks support concurrent read access from multiple processes; exclusive locks support exclusive write access. File locks provide protection across processes; they are ineffective for multiple threads within a single process. Both shared locks and exclusive locks eliminate the potential for a cross-process race condition on the locked region. Exclusive locks provide mutual exclusion; shared locks prevent alteration of the state of the locked file region (one of the required properties for a data race).
The Java API [[API 2006]] documentation states that, "Whether or not a lock actually prevents another program from accessing the content of the locked region is system-dependent and consequently unspecified."
Microsoft Windows uses a mandatory file-locking mechanism that prevents processes from accessing a locked file region.
Linux implements both mandatory locks and advisory locks. Advisory locks are not enforced by the operating system, which diminishes their value from a security perspective. Unfortunately, the mandatory file lock in Linux is generally impractical because
- Mandatory locking is only supported by certain network file systems.
- File systems must be mounted with support for mandatory locking, and this is disabled by default.
- Locking relies on the group ID bit, which can be turned off by another process (thereby defeating the lock).
- The lock is implicitly dropped if the holding process closes any descriptor of the file.
Noncompliant Code Example
In this noncompliant code example, an attacker could specify the name of a locked device or a FIFO file, causing the program to hang when opening a file.
String file = /* provided by user */ InputStream in = null; try { in = new FileInputStream(file); // ... } finally { try { in.close(); } catch (IOException x) { // handle error } }
Noncompliant Code Example (Java 1.7)
This noncompliant code example uses the try-with-resources statement from Java 1.7 to open the file. While this guarantees the file's successful closure if an exception is thrown, it is subject to the same vulnerabilities as the previous example.
String filename = /* provided by user */ Path file = new File(filename).toPath(); try (InputStream in = Files.newInputStream(file)) { // read file } catch (IOException x) { // handle error }
Noncompliant Code Example (Java 1.7: isRegularFile()
)
This noncompliant code example first checks that the file is a regular file before opening it.
String filename = /* provided by user */ Path file = new File(filename).toPath(); try { BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class); // Check if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // other necessary checks // Use try (InputStream in = Files.newInputStream(file)) { // read file }; } catch (IOException x) { // handle error }
This test can still be circumvented by a symbolic link. By default, the readAttributes()
method follows symbolic links and reads the file attributes of the final target of the link. The result is that the program may reference a file other than the one intended.
Noncompliant Code Example (Java 1.7: NOFOLLOW_LINKS
)
This noncompliant code example checks the file by calling the readAttributes()
method with the NOFOLLOW_LINKS
link option to prevent the function from following symbolic links. This allows the detection of symbolic links because the isRegularFile()
is made on the symbolic link file and not on the final target of the link.
String filename = /* provided by user */ Path file = new File(filename).toPath(); try { BasicFileAttributes attr = Files.readAttributes( file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); // Check if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // other necessary checks // Use try (InputStream in = Files.newInputStream(file)) { // read file }; } catch (IOException x) { // handle error }
This code is still vulnerable to a TOCTOU race condition, however. For example, an attacker can replace the regular file with a file link or device file after the code has completed its checks but before it opens the file.
Noncompliant Code Example (Java 1.7: Check-Use-Check)
This noncompliant code example performs necessary checks and then opens the file. After opening the file, it performs a second check to make sure that the file has not been moved and that the file it opened is the same one it checked. This reduces the chance that an attacker has changed the file between checking and opening the file. In both checks, the file's fileKey
attribute is examined. This serves as a unique key for identifying files and is a more reliable indicator of a file's identity than its path name.
[[J2SE 2011]] documents the fileKey
attribute as follows:
Returns an object that uniquely identifies the given file, or null if a file key is not available. On some platforms or file systems it is possible to use an identifier, or a combination of identifiers to uniquely identify a file. Such identifiers are important for operations such as file tree traversal in file systems that support symbolic links or file systems that allow a file to be an entry in more than one directory. On UNIX file systems, for example, the device ID and inode are commonly used for such purposes.
The file key returned by this method can only be guaranteed to be unique if the file system and files remain static. Whether a file system re-uses identifiers after a file is deleted is implementation dependent and therefore unspecified.
File keys returned by this method can be compared for equality and are suitable for use in collections. If the file system and files remain static, and two files are the same with non-null file keys, then their file keys are equal.
String filename = /* provided by user */ Path file = new File(filename).toPath(); try { BasicFileAttributes attr = Files.readAttributes( file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); Object fileKey = attr.fileKey(); // Check if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // other necessary checks // Use try (InputStream in = Files.newInputStream(file)) { // Check BasicFileAttributes attr2 = Files.readAttributes( file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); Object fileKey2 = attr2.fileKey(); if (fileKey != fileKey2) { System.out.println("File has been tampered with"); } // read file }; } catch (IOException x) { // handle error }
While this code example goes to great lengths to prevent an attacker from successfully tricking it into opening the wrong file, it still has several vulnerabilities:
- A TOCTOU race condition exists between the first check and open. During this race window, an attacker can replace the regular file with a symbolic link or other non-regular file. The second check detects this race condition but does not eliminate it; an attacker can still cause the system to block when opening the file.
- An attacker could subvert this code by letting the check operate on a normal file, substituting the non-normal file for the open, and then resubstituting the normal file to circumvent the second check. This vulnerability exists because Java lacks any mechanism to obtain file attributes from a file by any means other than the file name, and the binding of the file name to a file object is reasserted every time the file name is used in an operation. Consequently, an attacker can still switch out a file for a nefarious file, such as a symbolic link.
- A system with hard links allows an attacker to construct a malicious file that is a hard link to a sensitive file. Hard links cannot be reliably detected by a program and serve as a foil to canonicalization attempts, which are prescribed by IDS02-J. Canonicalize path names before validating them.
Compliant Solution (POSIX, Java 1.7, secure directory)
Because of the potential for race conditions and the inherent accessibility of shared directories, files must only be operated upon in secure directories. Because programs may run with reduced privileges and lack the facilities to construct a secure directory, a program may need to throw an exception if it can determine that a given path name is not in a secure directory.
Following is an implementation of an isInSecureDir()
method. This method ensures that the supplied file and all directories above it are owned by either the user or the superuser, that each directory lacks write access for any other users, and that directories above the given file may not be deleted or renamed by any other users (except the superuser).
/** * Indicates if file lives in a secure directory relative to the program's user * @param file Path to test * @return true if file's directory is secure */ public static boolean isInSecureDir(Path file) { return isInSecureDir( file, null); } /** * Indicates if file lives in a secure directory relative to the program's user * @param file Path to test * @param user User to test. If null defaults to current user * @return true if file's directory is secure */ public static boolean isInSecureDir(Path file, UserPrincipal user) { if (!file.isAbsolute()) { file = file.toAbsolutePath(); } // Get UserPincipal for specified user and superuser FileSystem fileSystem = Paths.get(file.getRoot().toString()).getFileSystem(); UserPrincipalLookupService upls = fileSystem.getUserPrincipalLookupService(); UserPrincipal root = null; try { root = upls.lookupPrincipalByName("root"); if (user == null) { user = upls.lookupPrincipalByName(System.getProperty("user.name")); } if (root == null || user == null) { return false; } } catch (IOException x) { return false; } // If any parent dirs (from root on down) are not secure, dir is not secure for (int i = 1; i <= file.getNameCount(); i++) { Path partialPath = Paths.get(file.getRoot().toString(), file.subpath(0, i).toString()); try { if (Files.isSymbolicLink(partialPath)) { if (!isInSecureDir(Files.readSymbolicLink(partialPath))) { // Symbolic link, linked-to dir not secure return false; } } else { UserPrincipal owner = Files.getOwner(partialPath); if (!user.equals( owner) && !root.equals( owner)) { // dir owned by someone else, not secure return false; } PosixFileAttributes attr = Files.readAttributes(partialPath, PosixFileAttributes.class); Set<PosixFilePermission> perms = attr.permissions(); if (perms.contains(PosixFilePermission.GROUP_WRITE) || perms.contains(PosixFilePermission.OTHERS_WRITE)) { // someone else can write files, not secure return false; } } } catch (IOException x) { return false; } } return true; }
When checking directories, it is important to traverse from the root to the leaf to avoid a dangerous race condition whereby an attacker who has privileges to at least one of the directories can rename and recreate a directory after the privilege verification.
The file name passed to this function is first rendered absolute if necessary. If the path contains any symbolic links, this routine will recursively invoke itself on the linked-to directory and ensure it is also secure. A symlinked directory may be secure if both its source and linked-to directory are secure. The function checks every directory in the path, ensuring that every directory is owned by the current user or the superuser and that all directories in the path forbid other users from deleting or renaming files.
On POSIX systems, disabling group and other write access to a directory prevents modification by anyone other than the owner of the directory and the system administrator.
Note that this method is only effective on file systems that are fully compatible with UNIX permissions, and it may not behave correctly for file systems with other permission mechanisms, such as AFS.
This compliant solution uses the isInSecureDir()
method to ensure that an attacker cannot tamper with the file to be opened and subsequently removed. Note that once the path name of a directory has been checked using isInSecureDir()
, all further file operations on that directory must be performed using the same path. This compliant solution also makes sure the requested file is indeed a regular file, and not a symbolic link, device file, etc.
String file = /* provided by user */ try { Path path = Paths.get( file); if (!isInSecureDir( path)) { System.out.println("File not in secure directory"); return; } BasicFileAttributes attr = Files.readAttributes( path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); // Check if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // other necessary checks try (InputStream in = Files.newInputStream(file)) { // read file } } catch (IOException x) { // handle error }
Programs with elevated privileges may need to write files to directories owned by unprivileged users. One example would be a mail daemon that reads a mail message from one user and places it in a directory owned by another user. In such cases, the proper course of action is to assume the privileges of a user when reading or writing files on behalf of that user, in which case all file access should occur in secure directories relative to that user. If a program with elevated privileges must write files on its own behalf, then these files should be in secure directories relative to the privileges of the program (such as directories accessible only by the superuser).
Exceptions
FIO00-EX0: Programs that operate on single user systems, or on systems where there are no shared directories or no possibility of file system vulnerabilities, do not need to ensure that files are maintained in secure directories before operating on them.
Risk Assessment
Allowing operations to be performed on files in shared directories can result in denial-of-service attacks. If the program has elevated privileges, then privilege escalation exploits become possible.
Rule |
Severity |
Likelihood |
Remediation Cost |
Priority |
Level |
---|---|---|---|---|---|
FIO00-J |
medium |
unlikely |
medium |
P4 |
L3 |
Related Guidelines
FIO32-C. Do not perform operations on devices that are only appropriate for files |
|
FIO32-CPP. Do not perform operations on devices that are only appropriate for files |
|
CWE ID 67, "Improper Handling of Windows Device Names" |
Bibliography
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="29066a9c-8d97-4c9f-8ba7-0763b7e74871"><ac:plain-text-body><![CDATA[ |
[[API 2006 |
AA. Bibliography#API 06]] |
Class File, methods |
]]></ac:plain-text-body></ac:structured-macro> |
|
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="f3c613af-344c-41e9-bf38-3f8f593096f4"><ac:plain-text-body><![CDATA[ |
[[CVE 2008 |
AA. Bibliography#CVE 08]] |
[CVE-2008-5354 |
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2008-5354] |
]]></ac:plain-text-body></ac:structured-macro> |
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="84ea7599-826a-4e3f-a5cb-04f80a7e1b85"><ac:plain-text-body><![CDATA[ |
[[Darwin 2004 |
AA. Bibliography#Darwin 04]] |
11.5 Creating a Transient File |
]]></ac:plain-text-body></ac:structured-macro> |
|
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="684cf7ad-ae8d-4725-901a-34ca4e1e27c1"><ac:plain-text-body><![CDATA[ |
[[Garfinkel 1996 |
AA. Bibliography#Garfinkel 96]] |
Section 5.6, "Device Files" |
]]></ac:plain-text-body></ac:structured-macro> |
|
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="e39132c9-54ac-46a1-85e6-6f56ee897c7b"><ac:plain-text-body><![CDATA[ |
[[Howard 2002 |
AA. Bibliography#Howard 02]] |
Chapter 11, "Canonical Representation Issues" |
]]></ac:plain-text-body></ac:structured-macro> |
|
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="9fb772c8-eb08-403a-ad24-b9dd49176724"><ac:plain-text-body><![CDATA[ |
[[J2SE 2011 |
AA. Bibliography#J2SE 11]] |
The try-with-resources Statement |
]]></ac:plain-text-body></ac:structured-macro> |
|
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="015d5c14-2ce5-4c06-a99c-658460d71b0b"><ac:plain-text-body><![CDATA[ |
[[Open Group 2004 |
AA. Bibliography#Open Group 04]] |
[ |
http://www.opengroup.org/onlinepubs/009695399/functions/open.html] |
]]></ac:plain-text-body></ac:structured-macro> |
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="2bb10212-fba7-4602-83cb-8f77bd5a4ef6"><ac:plain-text-body><![CDATA[ |
[[SDN 2008 |
AA. Bibliography#SDN 08]] |
Bug IDs: 4171239, 4405521, 4635827, 4631820 |
]]></ac:plain-text-body></ac:structured-macro> |
|
<ac:structured-macro ac:name="unmigrated-wiki-markup" ac:schema-version="1" ac:macro-id="2199e248-a5c5-4583-b23c-505a86bfcfb4"><ac:plain-text-body><![CDATA[ |
[[Secunia 2008 |
AA. Bibliography#Secunia 08]] |
[Secunia Advisory 20132 |
http://secunia.com/advisories/20132/] |
]]></ac:plain-text-body></ac:structured-macro> |