This article describes the various sorts of race conditions and insecure file operations and discusses how to avoid them. Code samples illustrate safe practices.
Suppose you wrote a program designed to automatically count the number of people entering a sports stadium for a game. The turnstiles are wired and send a signal to the computer each time someone walks through. You have a separate process running to monitor the signal from each turnstile. Each time a process receives a signal, it reads the global variable Gate
, increments it by one, and writes it back. Thus, multiple processes are keeping a single running total. Now suppose two people enter different gates at exactly the same time. The sequence of events might then be as follows:
Process A receives a signal from gate A.
Process B receives a signal from gate B.
Process A reads Gate == 1000
.
Process B reads Gate == 1000
.
Process A increments Gate
by 1 so that Gate == 1001
.
Process B increments Gate
by 1 so that Gate == 1001
.
Process A writes Gate = 1001
.
Process B writes Gate = 1001
.
Because process B read Gate
before process A had time to increment it and write it back, both process A and process B have read the same value for Gate
. After process A increments Gate
and writes it back, process B overwrites the value of Gate
with the same value written by process A. Because of the race condition, one of the two people entering the stadium was not counted. Since there might be long lines at each turnstile, this condition might occur many times before a big game, and a dishonest ticket clerk who knew about this undercount could pocket some of the receipts with no fear of being caught.
From a software security point of view, there are a couple of ways to exploit race conditions. If a program is writing temporary files, or temporarily relaxing permissions on files or folders in order to perform a privileged operation, an attacker might be able to create a race condition by careful timing of his attack. If the program checks the status of a file before writing to it, for example, the attacker might be able to take advantage of the time gap between when the program checks the file and when it writes to it to mount an attack. This is referred to as a time of check–time of use problem.
Other race conditions that can be exploited, like the example above, involve the use of shared data or other interprocess communication methods. If an attacker can interfere with the data after it is written and before it is read, he can disrupt the operation of the program, alter data, or do other mischief. The use of non-thread-safe calls in multithreaded programs can result in data corruption. If an attacker can manipulate the program to cause two such threads to interfere with each other, it may be possible to mount a denial-of-service attack. In some cases, by using such a race condition to overwrite a buffer in the heap with data from a routine that uses more data than the routine that allocated the buffer, an attacker can create a buffer overflow. As discussed in “Avoiding Buffer Overflows,” buffer overflows can be exploited to cause execution of malicious code. Darwin-level code (that is, scripts and code written with direct calls to BSD) that includes signal handlers is especially vulnerable to this sort of attack.
Any time the sequence in which two operations are completed affects the result, there is the potential for a race condition. For example, if two processes (in a single program or different programs) share the same global variable, then there is the potential for one process to interfere with the other or for an attacker to alter the variable after one process sets it but before the other reads it. See “Race Conditions Explained” at the beginning of this article for an example of a race condition of this type. The solution to race conditions of this type is to use some locking mechanism to prevent one process from changing a variable until another is finished with it. There are problems and hazards associated with such mechanisms, however, and they must be implemented carefully. For a full discussion, see Wheeler, Secure Programming for Linux and Unix HOWTO, at http://www.dwheeler.com/secure-programs/.
Signal handlers are another common source of race conditions. Signals from the operating system to a process or between two processes are used for such purposes as terminating a process or causing it to reinitialize. If you include signal handlers in your program, they should not make any system calls and should terminate as quickly as possible. Although there are certain system calls that are safe from within signal handlers, writing a safe signal handler that does so is tricky. The best thing to do is to set a flag that your program checks periodically, and do no other work within the signal handler. This is because the signal handler can be interrupted by a new signal before it finishes processing the first signal, leaving the system in an unpredictable state or, worse, providing a vulnerability for an attacker to exploit. For example, if the signal handler writes user-supplied data to a system log, an attacker can use a signal handler race condition to put the attacker's own code into the heap.
In a vulnerability reported in 1997 for a number of implementations of the FTP protocol, a user could cause a race condition by closing an FTP connection. Closing the connection resulted in the near-simultaneous transmission of two signals to the FTP server: one to abort the current operation, and one to log out the user. The race condition occurred when the logout signal arrived just before the abort signal. When a user logged onto an FTP server as an anonymous user, the server would temporarily downgrade its privileges from root to nobody so that the logged-in user had no privileges to write files. In order to log out the user, however, the server reassumed root privileges. If the abort signal arrived at just the right time, it would abort the logout procedure after the server had assumed root privileges but before it had logged out the user. The user would then be logged in with root privileges, and could proceed to write files at will. An attacker could exploit this vulnerability with a graphical FTP client simply by repeatedly clicking the “Cancel” button. [CVE-1999-0035]
For a brief introduction to signal handlers, see the Little Unix Programmers Group site at http://users.actcom.co.il/~choo/lupg/tutorials/signals/signals-programming.html. For a discourse on how signal handler race conditions can be exploited, see the article by Michal Zalewski at http://www.bindview.com/Services/razor/Papers/2001/signals.cfm.
Insecure file operations are a major source of security vulnerabilities. In some cases, opening or writing to a file in an insecure fashion can give attackers the opportunity to create a race condition (see “Time Of Check–Time Of Use”). Often, however, insecure file operations give an attacker the chance to read confidential information, an opportunity to gain control of an application or even of the system, or an opening for a denial of service attack. This section discusses insecure file operations. The following section, “Secure File Operations,” describes some techniques you can use to make sure your file operations are secure.
The chflags
utility sets flags on a file, including a flag that prevents the file from being modified. Once this flag has been set, attempts to modify the file—such as to change permissions with the chmod
utility or to change the UID or GID with the chown
utility—will fail, even if these utilities are run as root. Therefore, you must always check result codes of file operations and be prepared to handle the situation if the operation fails.
Although the rm
command clears user flags on a file if it sees that they're there, it can still fail. For example, you can't remove a directory that has anything inside it. If a directory is in a location where other users have access to it, any attempt to remove the directory might fail. The safest thing is to use a private directory that no one else has access to. If that’s not possible, check to make sure the rm
command succeeded and be prepared to handle the case that it does not.
A hard link is a second name for a file—the file appears to be in two different locations with two different names. If a file has two (or more) hard links and you check the file to make sure that the ownership, permissions, and so forth are all correct, but fail to check the number of links to the file, an attacker can write to or read from the file through their own link in their own directory. Therefore, among other checks before you use a file, you should check the number of links. Do not, however, simply fail if there's a second link to a file, because there are some circumstances where a link is all right or even expected. You need to anticipate such conditions and allow for them. Even if the link is unexpected, you need to handle the situation gracefully. Otherwise, an attacker can cause denial of service just by creating a link to the file. Instead, you should notify the user of the situation, giving them as much information as possible so they can try to track down the source of the problem.
Symbolic links are more common than hard links. A symbolic link is a special type of file that contains a path name. Functions that follow symbolic links automatically open, read, or write to the file whose path name is in the symbolic link file rather than the symbolic link file itself. Your application receives no notification that a symbolic link was followed; to your application, it appears as if the file addressed is the one that was used. An attacker can use a symbolic link, for example, to cause your application to write the contents intended for a temporary file to a critical system file instead, thus corrupting the system. Alternatively, the attacker can capture data you are writing or can substitute the attacker's data for your own when you read the temporary file. Avoid functions, such as chown
and stat
, that follow symbolic links (see Table 1 for alternatives). As with hard links, your program should evaluate whether a symbolic link is all right, and if not, should handle the situation gracefully.
Any time you work on files in a location to which others have read/write access, there’s the potential for the file to be compromised or corrupted.
Before you attempt a file operation, make sure the operation can be done on that file. For example, before attempting to read a file, make sure it's not a FIFO.
Just because you can write to a file, that doesn’t mean you should write to it. For example, the fact that a directory exists doesn’t mean you created it, and the fact that you can append to a file doesn’t mean you own the file or no one else can write to it.
Mac OS X can perform file operations on files in several different file systems. Some operations can be done only on certain systems. For example, certain file systems honor setuid files when executed from them and some don't. Be sure you know what file system you’re working with and what operations can be carried out on that system.
Local pathnames can point to remote files. For example, the path /volumes/foo
might actually be someone's FTP server rather than a locally-mounted volume. Just because you're accessing something by a pathname, that does not guarantee that it's local or that it should be accessed.
A user can mount a file system anywhere they have write access and own the directory. In other words, almost anywhere a user can create a directory, they can mount a file system on top of it. Because this can be done remotely, an attacker running as root on a remote system could mount a file system into your home directory. Files in that file system would appear to be files in your home directory owned by root. For example, /tmp/foo
might be a local directory, or it might be the root mount point of a remotely mounted file system. Similarly, /tmp/foo/bar
might be a local file, or it might have been created on another machine and be owned by root over there. Therefore, you can't trust files based only on ownership, and you can't assume that setting the UID to 0 was done by someone you trust. To tell whether the file is mounted locally, use the lstat
or fstat
call to check the device ID. If the device ID is different from that of files you know to be local, then you’ve crossed a device boundary.
Just because a program's executable doesn't mean that users won't be able to read the content of the file.
When you fork a new process, the child process inherits all the file descriptors from the parent unless you set the close-on-exec flag. If you fork and execute a child process and drop the child process’ privileges so its real and effective IDs are those of some other user (to avoid running that process with elevated privileges), then that user can use a debugger to attach the child process. They can then run arbitrary code from that running process. Because the child process inherited all the file descriptors from the parent, the user now has access to every file opened by the parent process. See “Inheriting File Descriptors” for more information on this type of vulnerability.
There are several principles you can follow to help ensure that you do not have file-based security vulnerabilities in your program:
The first principle is to always check the result codes of all the routines you call. Most of the file-based security vulnerabilities that have been caught by Apple's security team could have been avoided if the developers of the programs had checked result codes. For example, if someone has called the chflags
utility to set the immutable flag on a file and you call the chmod
utility to change file modes or access control lists on that file, then your chmod
call will fail, even if you are running as root. Another example of a call that might fail unexpectedly is the rm
call to delete a directory. If you think a directory is empty and call rm
to delete the directory, but someone else has put a file or subdirectory in there, your rm
call will fail.
When working in a directory to which your process does not have exclusive access, you must check to make sure a file does not exist before you create it. You must also verify that the file you intend to read from or write to is the same file you created.
Toward this end, use routines that operate on file descriptors rather than pathnames wherever possible, so you can be sure you're always dealing with the same file.
Intentionally create files as a separate step from opening them so that you can verify that you are opening a file you created rather than one that already existed.
When checking for the existence or status of a file, you must know whether the function or shell routine you are calling follows symbolic links. For example, whereas the C function lstat
gives you the status of a file regardless of whether it's a normal file or a symbolic link, the stat
function follows symbolic links and, if the specified file was a symbolic link, returns the status of the linked-to file. Therefore, if you use the stat
function, you could be fooled into thinking you are writing to or reading from a certain file in a known directory, when you are really accessing another file entirely.
Before you read a file, make sure it has the owner and permissions you expect. Be prepared to fail gracefully (rather than hanging) if it does not.
Set your process' file code creation mask (umask) to restrict access to files created by your process. The umask is a bitmask that alters the default permissions of a new file. If you set the umask to 0x022
, for example, any new file created by your process has rw-r--r--
permissions. When a process calls another process, the new process inherits the parent process' umask. Then if your process calls another process, the new process creates a file, and the new process does not reset the umask, you have a good chance of having a file that is not accessible to all users on the system. For more information on the umask, see the manual page for umask(2)
and Viega and McGraw, Building Secure Software, Addison Wesley, 2002. For a particularly lucid explanation of the use of a umask, see http://www.sun.com/bigadmin/content/submitted/umask_permissions.html.
The following sections give some hints on how to follow these principles when you are using generic C code, Carbon, and Cocoa.
For generic C programming, if you are opening a temporary file in a public directory, you can use the open
function with the O_CREAT
and O_EXCL
flags set to create the file and obtain a file descriptor. The O_EXCL
flag causes this function to return an error if the file already exists. Be sure to check for errors before proceeding. As a shortcut, you can use the mkstemp
function to open the temporary file. The mkstemp
function guarantees a unique filename and returns a file descriptor, thus allowing you skip the step of checking the open
function result for an error, which might require you to change the filename and call open
again.
After you've opened the file and obtained a file descriptor, you can safely use functions that take file descriptors, such as the standard C functions write
and read
, for as long as you keep the file open. See the manual pages for open(2)
, mkstemp(3)
, write(2)
, and read(2)
for more on these functions, and see Wheeler, Secure Programming for Linux and Unix HOWTO for advantages and shortcomings to using these functions.
If you need to open a preexisting file to modify it or read from it, you need to check the file's ownership, type, and permissions, and the number of links to the file before using it.
To safely opening a file for reading, for example, you can use the following procedure:
Call the lstat
function to get information about the file. (Do not use the stat
function, as that function follows symbolic links.) Save the stat
structure returned by the lstat
function.
Check the file's status information to make sure the file is not a symbolic link.
Check the user ID (UID) and group ID (GID) of the file to make sure they are correct.
Check the filetype to make sure it's correct.
Check the read, write, and execute permissions for the file to make sure they are what you expect.
Check that there is only one hard link to the file.
Call the open
function and save the file descriptor.
Using the file descriptor, call the fstat
function to obtain the stat
structure for the file you opened.
Compare the device and inode numbers in the stat structure obtained before you opened the file with those in the stat structure obtained after you opened the file to verify that they are the same file.
Check all the information in the stat
structure returned by the fstat
function to make sure it is what you expect.
Although this might seem like a lot of extra work, it eliminates the race condition that can occur between calling the stat
and open
functions. Note that you can avoid all the status checking by using a secure directory instead of a public one to hold your program's files.
Table 1 shows some functions to avoid—and the safer equivalent functions to use—in order to avoid race conditions when you are creating files in a public directory.
If you are using the Carbon File Manager to create and open files, you should be aware of how the File Manager accesses files.
The file specifier FSSpec
structure uses a path to locate files, not a file descriptor. Functions that use an FSSpec
file specifier are deprecated and should not be used in any case.
The file reference FSRef
structure uses a path to locate files and should be used only if your files are in a safe directory, not in a publicly accessible directory. These functions include FSGetCatalogInfo
, FSSetCatalogInfo
, FSCreateFork
, and others.
The File Manager creates and opens files in separate operations. The create operation fails if the file already exists. However, none of the file-creation functions return a file descriptor.
To find the default location to store temporary files, you can call the FSFindFolder
function and specify a directory type of kTemporaryFolderType
. This function checks to see whether the UID calling the function owns the directory and, if not, returns the user home directory in ~/library
. Therefore, this function returns a relatively safe place to store temporary files. This location is not as secure as a directory that you created and that is accessible only by your program. The FSFindFolder
function is documented in Folder Manager Reference.
If you've obtained the file reference of a directory (from the FSFindFolder
function, for example), you can use the FSRefMakePath
function to obtain the directory's path name. However, be sure to check the function result, because if the FSFindFolder
function fails, it returns a null
string. If you don't check the function result, you might end up trying to create a temporary file with a pathname formed by appending a filename to a null
string.
There are no Cocoa methods that create a file and return a file descriptor. However, you can call the standard C open
function from an Objective-C program to obtain a file descriptor (see “Generic C”). Or you can call the mkstemp
function to create a temporary file and obtain a file descriptor. Then you can use the NSFileHandle
method InitWithFileDescriptor:
to initialize a file handle, and other NSFileHandle
methods to safely write to or read from the file. Documentation for the NSFileHandle
class is in Foundation Framework Reference.
To obtain the path to the default location to store temporary files (stored in the $TMPDIR
environmental variable), you can use the NSTemporaryDirectory
function, which calls theFSFindFolder
and FSRefMakePath
functions for you (see “Carbon”). Note that NSTemporaryDirectory
can return /tmp
under certain circumstances such as if you link on a pre-Mac OS X v10.3 development target. Therefore, if you're using NSTemporaryDirectory
, you either have to be sure that using /tmp
is suitable for your operation or, if not, you should consider that an error case and create a more secure temporary directory if that happens.
The changeFileAttributes:atPath:
method in the NSFileManager
class is similar to chmod
or chown
, in that it takes a file path rather than a file descriptor. You shouldn't use this method if you're working in a public directory or a user's home directory. Instead, call the fchown
or fchmod
function (see Table 1). You can call the NSFileHandle
class's fileDescriptor
method to get the file descriptor of a file in use by NSFileHandle
.
The NSString
and NSData
classes have writeToFile:atomically
methods designed to minimize the risk of data loss when writing to a file. These methods write first to a temporary file, and then, when they're sure the write is successful, they replace the written-to file with the temporary file. This is not always an appropriate thing to do when working in a public directory or a user's home directory, because there are a number of path-based file operations involved. Instead, initialize an NSFileHandle
object with an existing file descriptor and use NSFileHandle
methods to write to the file, as mentioned above. The following code, for example, uses the mkstemp
function to create a temporary file and obtain a file descriptor, which it then uses to initialize NSFileHandle
:
fd = mkstemp(tmpfile); // check return for -1, which indicates an error |
NSFileHandle *myhandle = [[NSFileHandle alloc] initWithFileDescriptor:fd]; |
Scripts must follow the same general rules as other programs to avoid race conditions. There are a few tips you should know to help make your scripts more secure.
First, when writing a script, set the temporary directory ($TMPDIR
) environmental variable to a safe directory. Even if your script doesn't directly create any temporary files, one or more of the routines you call might create one, which can be a security vulnerability if it's created in an insecure directory. See the manual pages for setenv(1)
and setenv(3)
for information on changing the temporary directory environmental variable. For the same reason, set your process' file code creation mask (umask) to restrict access to any files that might be created by routines run by your script (see “Secure File Operations” for more information on the umask).
It's also a good idea to use the ktrace
function on a shell script so you can watch every command that gets executed to make sure that during the life of your script no temporary file is created in an insecure location. See the manual page for ktrace(2)
for more information.
Do not redirect output using the operators >
or >>
to a publicly writable location. These operators do not check to see whether the file already exists, and they follow symbolic links.
Do not use the test
command (or its left bracket ([
) equivalent) to check for the existence of a file or other status information for the file before writing to it. Doing so always results in a race condition; that is, it is possible for an attacker to create, write to, alter, or replace the file before you start writing. Instead, use the mkdtemp
command to create a subdirectory to which only you have access. It's important to check the result to make sure the command succeeded. if you do all your file operations in this directory, you can be fairly confident that no one with less than root access can interfere with your script. For more information, see the manual pages for test(1)
and mkdtemp(3)
.
A race condition that can be caused by insecure file operations is the time of check–time of use problem. Many programs write temporary files to publicly accessible directories. You can set the file permissions of the temporary file to prevent another user from altering the file. However, if the file already exists before you write to it, you could be overwriting data needed by another program or you could be using a file prepared by an attacker, in which case it might be a symbolic link, redirecting your output to a file needed by the system or to a file controlled by the attacker. To prevent this, programs often check to make sure a temporary file with a specific name does not already exist in the target directory, and then they open the file to write to it.
An attacker can create a race condition by repeatedly creating and removing files with the name used by your program for the temporary file. If they create the file at just the right moment, it will be after your program has checked to make sure the file doesn't exist, but before the program writes to it. Then, when your program does write to the temporary file, it will be writing to the attacker's file rather than creating a new file.
In a vulnerability in a directory server, a server script was executing commands to write private and public keys to temporary files, then reading those keys and putting them in a database. Because the temporary files were in a publicly writable directory, an attacker could have created a race condition by substituting the attacker's own files (or symbolic links to the attacker's files) before the keys were read, thus causing the script to read the attacker's private and public keys. After that, anything encrypted or authenticated using those keys would be under the attacker's control. Or the attacker could have read the private keys, which can be used to decrypt encrypted data. Private keys must be kept secret to be useful. [CVE-2005-2519]
Often, rather than substituting an ordinary file for your temporary file, an attacker creates a hard or symbolic link.
Here are some guidelines to help you avoid time of check–time of use vulnerabilities. For more detailed discussions, especially for C code, see Viega and McGraw, Building Secure Software, Addison Wesley, 2002, and Wheeler, Secure Programming for Linux and Unix HOWTO, available at http://www.dwheeler.com/secure-programs/.
If at all possible, avoid creating temporary files in a shared directory, such as /tmp
, or in directories owned by the user. If anyone else has access to your temporary file, they can modify its content, change its ownership or mode, or replace it with a hard or symbolic link. It's much safer to either not use a temporary file at all (use some other form of interprocess communication) or keep temporary files in a directory you create and to which only your process (acting as your user) has access.
If your file must be in a shared directory, give it a unique (and randomly generated) filename (you can use the C function mkstemp
to do this) and never close and reopen the file. If you close such a file, an attacker can potentially find it and change it before you reopen it. Here are some public directories that you can use:
~/Library/Caches/TemporaryItems
When you use this subdirectory, you are writing to the user's own home directory, not some other user's directory or a system directory. If the user's home directory has the default permissions, it can be written to only by that user and root. Therefore, this directory is not as susceptible to attack from outside, nonprivileged users as some other directories might be.
/var/run
This directory is used for process ID (pid) files and other system files needed just once per startup session. This directory is cleared out each time the system starts up.
/var/db
This directory is used for databases accessible to system processes.
Last updated: 2010-02-12