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.

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

  • mandatory locking is supported only by certain network file systems.
  • file systems must be mounted with support for mandatory locking, which 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

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:

  • The TOCTOU race condition still exists between the first check and open. During this race window, an attacker can replace the regular file with a symbolic link or other nonregular file. The second check detects this race condition but does not eliminate it.
  • A system with hard links allows an attacker to construct a malicious file that is a hard link to a protected file. Hard links cannot be reliably detected by a program and can foil canonicalization attempts, which are prescribed by IDS00-PL. Canonicalize path names before validating them.

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:

  • checking group IDs
  • checking the file's sticky bit
  • checking that the file has only one hard link
  • permitting a set of trusted users in addition to the current user

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

Bibliography

 


7 Comments

  1. the new name seems like a good start.  I would still separate out the device stuff.

  2. I may be missing something, but I don't understand how checking is_secure_path and then later opening the file is secure.  Surely an attacker could change the filesystem after is_secure_path is called but before the file is opened.  The permission bits and file owners can also be changed...

    1. I've tweaked the text to the CS; it should be clearer now. Basically, if is_secure_path returns true, then an attacker lacks the permissions to change the filesystem in a way to cause the opening to fail or open the wrong file.

      1. The point needs to be clearer that is_secure_path protects you from an attacker running as a different user on the same system; I think it does not protect from race conditions that can be caused by other code running as the same user.  You can easily create a directory hierarchy, call is_secure_path, and then in the time between that call finishing and the file being opened, replace the directories with something else.

        You may say that 'attacks' by code running as the same user are not worth worrying about, and I would broadly agree.  There are certainly scenarios you can imagine where code running as user X might somehow be tricked into creating symbolic links to exploit other code also running as X, but they are far-fetched, especially when you consider the timing needed to exploit the race condition.

        So I think you should go into just a little bit more detail when asserting that 'no race conditions are possible', and make it clear that this applies only to other users on the system doing filesystem operations.  This will avoid the reader's instant reaction of 'what? no way!' when seeing that checking the path and opening the file by name happen as two separate operations, allowing time for the filesystem to change in the meantime.

        Also, what about an NFS server under the attacker's control?

        1. True, this is_secure_path approach does not address race conditions where both control flows are done by trusted users (eg the user running the program, or the superuser); I've qualified the text to make this more clear.

          WRT NFS: It depends on what kind of control the attacker has on the NFS mount. If they can access otherwise 'trusted' files, then this solution will not prevent them from exploiting race vulnerabilities. I don't know that much about NFS, but I thought it uses UNIX-style file permissions; an NFS attacker who could access your files (even if they don't have the appropriate perms) would have clearly compromised the UNIX permissions model; this solution does assume the UNIX permissions model is sound (smile)

          Incidentally, I don't agree that "attacks by code running as the same user are not worth worrying about"; I just think they cannot be mitigated by this technique (and often can't be mitigated at all; only prevented).

    2. Edward,

      A few of snippets from above:

      (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 algorithm into falsely reporting that a path is secure."

      (2) "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."

      (3) "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."

       

      With the above noted, and per my understanding and logic obtained within the information given regrading what the applications expected owner, group and world attributes and settings should be, a focus on (3) should prevent an attacker from changing the filesystem, unless of course the attacker is the same owner of the director(y|ies) being traversed, which in that case, this code would not be deemed safe (which may be the scenario and case you had in mind).

      It's also mentioned that:

      (4) "This is a reasonable definition of a secure path, but it could do other checks as well, such as:

      • checking group IDs
      • checking the file's sticky bit
      • checking that the file has only 1 hard link
      • permitting a set of trusted users, in addition to the current user"

      With (4) in mind, I think the code could also implement the checks done in the non-compliant code as well, without doing harm.  In particular a couple of the values returned from lstat compared with values returned later from POSIX::fstat,

       

       

      1. Ah, I ran in to a TOCTOS(submit :) and David answered you before I was finished replying/saving.

        Cheers.