Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

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 what users are trusted. For example, the mail daemon should only trust the user sending the message when gathering the mail to be sent, and then only trust 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. 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.

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:

Code Block
/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.

Code Block
bgColor#ffcccc
langperl
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.

Code Block
bgColor#ffcccc
langperl
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() builtin 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 will provide information about the link itself, rather than the file. So this code will correctly identify a symbolic link as 'not being a regular file'.

Code Block
bgColor#ffcccc
langperl
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 is accomplished using POSIX::fstat(), which returns the same information as lstat(), but operates on open file descriptors, rather than file names. This 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 and is a more reliable indicator of the file's identity than its path name.

Code Block
bgColor#ffcccc
langperl
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

While 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 rule 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. 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.

Code Block
bgColor#ccccff
langperl
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 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:

  • 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

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.

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, and so there is no check after the open; they only remaining check necessary is the check that the file is a regular file.

Code Block
bgColor#ccccff
langperl
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

P14

L3

Related Guidelines

CERT C Secure Coding Standard: FIO15-C. Ensure that file operations are performed in a secure directory

The CERT Oracle Secure Coding Standard for Java: FIO00-J. Do not operate on files in shared directories]

CERT C++ Secure Coding Standard: FIO15-CPP. Ensure that file operations are performed in a secure directory

Bibliography

[CPAN] POSIX
[Garfinkel 1996] Section 5.6, "Device files"

[Howard 2002] Chapter 11, "Canonical Representation Issues"

[Open Group 08]

Vulnerability Note VU#570952: "Redhat Linux diskcheck.pl creates predictable temporary file and fails to check for existing symbolic link of same name'

[Wall 2011perlfunc