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, a wide variety of file system vulnerabilities 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 when programs that access these files run with elevated privileges. Many file system properties and capabilities can be exploited by an attacker, including file links, device files, and shared file access. To prevent vulnerabilities, a program must operate only on files in secure directories.

The security of a directory depends on the security policy applied to a program. A program typically runs with the privileges of a particular user, and its policy will regard that user as trusted. Its policy may also regard some or all other users as untrusted. Any reasonable security policy must also regard the root user as trusted, as there is no protection from a malicious user with root privileges. For a particular security policy that may apply to a program, a directory is secure if only trusted users are allowed to create, move, or delete files inside that directory. Furthermore, each parent directory must itself be a secure directory up to and including the root directory. On most systems, home or user directories are secure by default and only explicitly shared directories, such as /tmp, are insecure.

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. Any such program should have a security policy dictating which users are trusted. For example, the mail daemon should trust only the user sending the message when gathering the mail to be sent and then trust only the user receiving the message when delivering it.

File Links

Many operating systems support file links, including symbolic (soft) links, hard links, shortcuts, shadows, aliases, and junctions. In POSIX, symbolic links can be created using the ln -s command and hard links using the ln command. Hard links are indistinguishable from normal files on POSIX systems.

Three file link types are supported in Windows NTFS (New Technology 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 a file being opened may actually be a link to a different file. This is especially dangerous when the vulnerable program is running with elevated privileges. When creating new files, an application running with elevated privileges may erroneously overwrite an existing file that resides outside the directory it expected.

Device Files

File names on many operating systems may be used to access device files. Device files are used to access hardware and peripherals. Reserved MS-DOS device names include AUX, CON, PRN, COM1, and LPT1. Character special files and block special files are POSIX device files that direct operations on the files to the appropriate device drivers.

Performing operations on device files intended only for ordinary character or binary files can result in crashes and denial-of-service (DoS) attacks. For example, when Windows attempts to interpret a device name as a file resource, it performs an invalid resource access that usually results in a crash [Howard 2002].

Device files in POSIX can be a security risk when an attacker can trick a program into accessing them in an unauthorized way. For instance, if malicious programs can read or write to the /dev/kmem device file, they may be able to alter their own priority, user ID, or other attributes of their process, or they may simply crash the system. Similarly, access to disk devices, tape devices, network devices, and terminals being used by other processes can also lead to problems [Garfinkel 1996].

On Linux, it is possible to lock certain applications by attempting to read or write data on devices rather than files. Consider the following device path names:

/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 website 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 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.

Some platforms provide various forms of file locking. Shared locks support concurrent read access from multiple processes; exclusive locks support exclusive write access. File locks provide protection across processes, but they do not provide protection from 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).

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

Noncompliant Code Example

This noncompliant code example opens a file whose name is provided by the user. It calls croak() if the open was unsuccessful, as required by EXP30-PL. Do not use deprecated or obsolete functions or modules.

use Carp;

my $file = # provided by user
open( my $in, "<", $file) or croak "error opening $file";
# ... work with FILE and close it

Unfortunately, an attacker could specify the name of a locked device or a first in, first out (FIFO) file, causing the program to hang when opening the file.

Noncompliant Code Example (Regular File)

This noncompliant code example first checks that the file is a regular file before opening it.

use Carp;
use Fcntl ':mode';
my $path = $ARGV[0]; # provided by user

# Check that file is regular
my $mode = (stat($path))[2] or croak "Can't run stat";
croak "Not a regular file" if (S_IFREG & $mode) == 0;

open( my $in, "<", $path) or croak "Can't open file";
# ... work with FILE and close it

This test can still be circumvented by a symbolic link. By default, the stat() built-in function 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 (lstat())

This noncompliant code example gets the file's information by calling lstat() rather than {[stat()}}. The lstat() system call does not follow symbolic links, and it provides information about the link itself rather than the file. Consequently, this code correctly identifies a symbolic link as not being a regular file.

use Carp;
use Fcntl ':mode';

my $path = $ARGV[0]; # provided by user

# Check that file is regular
my $mode = (lstat($path))[2] or croak "Can't run lstat";
croak "Not a regular file" if (S_IFREG & $mode) == 0;

open( my $in, "<", $path) or croak "Can't open file";
# ... work with FILE and close it

This code is still vulnerable to a time-of-check, time-of-use (TOCTOU) race condition. 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 (Check-Use-Check)

This noncompliant code example performs the necessary lstat() check and then opens the file using the POSIX::open() function to obtain a file descriptor. After opening the file, it performs a second check to make sure that the file has not been moved and that the file opened is the same file that was checked. This check is accomplished using POSIX::fstat(), which returns the same information as lstat() but operates on open file descriptors rather than file names. It does leave a race window open between the first check and the open but subsequently detects if an attacker has changed the file during the race window. In both checks, the file's device and i-node attributes are examined. On POSIX systems, the device and i-node serve as a unique key for identifying files, which is a more reliable indicator of the file's identity than its path name.

use Carp;
use Fcntl ':mode';
use POSIX;

my $path = $ARGV[0]; # provided by user

# Check that file is regular
my ($device, $inode, $mode, @rest) = lstat($path) or croak "Can't run lstat";
croak "Not a regular file" if (S_IFREG & $mode) == 0;

my $fd = POSIX::open($path, O_RDONLY) or croak "Can't open file";
# note: fd is a POSIX file descriptor, NOT a perl filehandle!

my ($fdevice, $finode, $fmode, @frest) = POSIX::fstat($fd) or croak "Can't run fstat";
croak "File has been tampered with" if $fdevice ne $device or $finode ne $inode;

open( my $in, "<&", $fd) or croak "Can't open file descriptor";
# ... work with FILE and close it

Although this code goes to great lengths to prevent an attacker from successfully tricking it into opening the wrong file, it still has several vulnerabilities:

Compliant Solution (Secure Path)

Because of the potential for race conditions and the inherent accessibility of shared directories by untrusted users, files must be operated on only by secure paths. A secure path is a directory that cannot be moved or deleted by untrusted users. Furthermore, its parent path, grandparent path, and so on up to the root, must also be secure, and if the path includes any symbolic links, both the link's target path and the path containing the link must be secure paths. Because programs may run with reduced privileges and lack the facilities to construct a secure path, a program may need to abort if it determines that a given path is not secure.

Following is a POSIX-specific implementation of an is_secure_path() subroutine. This function ensures that the file in the supplied path and all directories above it are secure paths.

use Carp;
use Fcntl ':mode';

# Fail if symlinks nest more than this many times
my $max_symlinks = 5;

# Indicates if a particular path is secure, including that parent directories
# are secure. If path contains symlinks, ensures symlink targets are also secure
# Path need not be absolute and can have trailing /.
sub is_secure_path {
  my ($path) = @_;

  # trim trailing /
  chop $path if $path =~ m@/$@; # This could turn root dir into empty string.
  # make sure path is absolute
  $path = $ENV{"PWD"} . "/" . $path if $path !~ m@^/@;

  return is_secure_path_aux( $path, $max_symlinks);
}

# Helper to is_secure_path. Requires absolute path, w/o trailing /
# Also accepts empty string, interpreted as root path.
sub is_secure_path_aux {
  my ($path, $symlinks) = @_;

  # Fail if too many levels of symbolic links
  return 0 if $symlinks <= 0;

  # Fail if parent path not secure
  if ($path =~ m@(^.*)(/[^/]+)@) {
    my $parent = $1;
    return 0 if !is_secure_path_aux( $parent, $max_symlinks);
  } else {
    # No parent; path is root dir, proceed
    $path = "/";
  }

  # If path is symlink, check that linked-to path is also secure
  if (-l $path) {
    my $target = readlink $path or croak "Can't read symlink, stopped";
    return 0 if !is_secure_path_aux( $target, $max_symlinks-1);
  }

  return is_secure_dir( $path);
}


# Indicates if a particular path is secure. Does no checks on parent
# directories or symlinks.
sub is_secure_dir {
  my ($path) = @_;

  # We use owner uid and permissions mode, from path's i-node
  my ($dummy1, $dummy2, $mode, $dummy3, $uid, @dummy4)
    = lstat($path) or croak "Can't run lstat, stopped";

  # Fail if file is owned by someone besides current user or root
  return 0 if $uid != $> && $uid != 0;

  # Fail if file has group or world write permissions
  return 0 if S_IWGRP & $mode || S_IWOTH & $mode;

  return 1;
}

When checking directories, it is important to traverse from the root directory to the leaf directory to avoid a dangerous race condition whereby an attacker who can write to at least one of the directories would rename and re-create a directory after the privilege verification of subdirectories but before the verification of the tampered directory. An attacker could use this race condition to fool the algorithm into falsely reporting that a path is secure.

If the path contains any symbolic links, the code will recursively invoke itself on the linked-to directory and ensure it is also secure. A symlinked directory may be secure if and only if both its source and linked-to directory are secure.

On POSIX systems, disabling group and world write access to a directory prevents modification by anyone other than the owner of the directory and the system administrator; consequently, this function checks that each path lacks group or world write permissions. It also checks that a file is owned by either the user running the program or the system administrator. This is a reasonable definition of a secure path, but it could do other checks as well, such as the following:

This code is effective only on file systems that are fully compatible with POSIX file access permissions; it may behave incorrectly for file systems with other permission mechanisms such as AFS. It is designed to prevent untrusted users from creating race conditions based on the file system. It does not prevent race conditions in which both accesses to a file are performed by the user or the superuser.

The following compliant solution uses the is_secure_path() 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 is_secure_path(), all further file operations on that file must be performed using the same path. No race conditions are possible involving untrusted users, and so there is no need to perform any check after the open; the only remaining check necessary is the check that the file is a regular file.

use Carp;

my $file = $ARGV[0]; # provided by user

croak "Not a secure path" if !is_secure_path( $file);

# Check that file is regular
my $mode = (lstat($file))[2] or croak "Can't run lstat";
croak "Not a regular file" if (S_IFREG & $mode) == 0;

open( my $in, "<", $file) or croak "Can't open file";
# ... work with FILE and close it

Exceptions

FIO00:EX0: This recommendation does not apply to programs that run on a system with one user or where all the users are trusted.

Risk Assessment

Performing operations on files in shared directories can result in DoS attacks. If the program has elevated privileges, privilege escalation exploits are possible.

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

FIO01-PL

Medium

Unlikely

Medium

P4

L3

Related Guidelines

SEI CERT C Coding StandardFIO15-C. Ensure that file operations are performed in a secure directory
SEI CERT C++ Coding StandardVOID FIO15-CPP. Ensure that file operations are performed in a secure directory
CERT Oracle Secure Coding Standard for JavaFIO00-J. Do not operate on files in shared directories

Bibliography

[CPAN]POSIX
[Garfinkel 1996]Section 5.6, "Device files"
[Howard 2002]Chapter 11, "Canonical Representation Issues"
[Open Group 08] 
[VU#570952]Redhat Linux diskcheck.pl creates predictable temporary file and fails to check for existing symbolic link of same name
[Wall 2011]perlfunc