Access Control - Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Chapter 2. Access Control

Access control is a major issue for application developers. An application must always be sure to protect its resources from unauthorized access. This requires properly setting permissions on created files, allowing only authorized hosts to connect to any network ports, and properly handling privilege elevation and surrendering. Applications must also defend against race conditions that may occur when opening files—for example, the Time of Check, Time of Use (TOCTOU) condition. The proper approach to access control is a consistent, careful use of all APIs that access external resources. You must minimize the time a program runs with privileges and perform only the bare minimum of operations at a privileged level. When sensitive data is involved, it is your application's duty to protect the user's data from unauthorized access; keep this in mind during all stages of development.

2.1. Understanding the Unix Access Control Model

Problem

You want to understand how access control works on Unix systems.

Solution

Unix traditionally uses a user ID-based access control system. Some newer variants implement additional access control mechanisms, such as Linux's implementation of POSIX capabilities. Because additional access control mechanisms vary greatly from system to system, we will discuss only the basic user ID system in this recipe.

Discussion

Every process running on a Unix system has a user ID assigned to it. In reality, every process actually has three user IDs assigned to it: an effective user ID, a real user ID, and a saved user ID.[1] The effective user ID is the user ID used for most permission checks. The real user and saved user IDs are used primarily for determining whether a process can legally change its effective user ID (see Recipe 1.3).

In addition to user IDs, each process also has a group ID. As with user IDs, there are actually three group IDs: an effective group ID, a real group ID, and a saved group ID. Processes may belong to more than a single group. The operating system maintains a list of groups to which a process belongs for each process. Group-based permission checks check the effective group ID as well as the process's group list.

The operating system performs a series of tests to determine whether a process has permission to access a particular file on the filesystem or some other resource (such as a semaphore or shared memory segment). By far, the most common permission check performed is for file access.

When a process creates a file or some other resource, the operating system assigns a user ID and a group ID as the owner of the file or resource. The user ID is assigned the process's effective user ID, and the group ID is assigned the process's effective group ID.

To define the accessibility of a file or resource, each file or resource has three sets of three permission bits assigned to it. For the owning user, the owning group, and everyone else (often referred to as "world" or "other"), read, write, and execute permissions are stored.

If the process attempting to access a file or resource shares its effective user ID with the owning user ID of the file or resource, the first set of permission bits is used. If the process shares its effective group ID with the owning group ID of the file or resource, the second set of permission bits is used. In addition, if the file or resource's group owner is in the process's group membership list, the second set of permission bits is used. If neither the user ID nor the group ID match, the third set of bits is used. User ownership always trumps group ownership.

Files also have an additional set of bits: the sticky bit, the setuid bit, and the setgid bit. The sticky and setgid bits are defined for directories; the setuid and setgid bits are defined for executable files; and all three bits are ignored for any other type of file. In no case are all three bits defined to have meaning for a single type of file.

The sticky bit

Under normal circumstances, a user may delete or rename any file in a directory that the user owns, regardless of whether the user owns the file. Applying the sticky bit to a directory alters this behavior such that a user may only delete or rename files in the directory if the user owns the file and additionally has write permission in the directory. It is common to see the sticky bit applied to directories such as /tmp so that any user may create temporary files, but other users may not muck with them.

Historically, application of the sticky bit to executable files also had meaning. Applying the sticky bit to an executable file would cause the operating system to treat the executable in a special way by keeping the executable image resident in memory once it was loaded, even after the image was no longer in use. This optimization is no longer necessary because of faster hardware and widespread support for and adoption of shared libraries. As a result, most modern Unix variants no longer honor the sticky bit for executable files.

The setuid bit

Normally, when an executable file loads and runs, it runs with the effective user, real user, and saved user IDs of the process that started it running. Under normal circumstances, all three of these user IDs are the same value, which means that the process cannot adjust its user IDs unless the process is running as the superuser.

If the setuid bit is set on an executable, this behavior changes significantly. Instead of inheriting or maintaining the user IDs of the process that started it, the process's effective user and saved user IDs will be adjusted to the user ID that owns the executable file. This works for any user ID, but the most common use of setuid is to use the superuser ID, which grants the executable superuser privileges regardless of the user that executes it.

Applying the setuid bit to an executable has serious security considerations and consequences. If possible, avoid using setuid. Unfortunately, that is not always possible; Recipe 1.3 and Recipe 1.4 discuss the setuid bit and the safe handling of it in more detail.

The setgid bit

Applied to an executable file, the setgid bit behaves similarly to the setuid bit. Instead of altering the assignment of user IDs, the setgid bit alters the assignment of group IDs. However, the same semantics apply for group IDs as they do for user IDs with respect to initialization of a process's group IDs when a new program starts.

Unlike the setuid bit, the setgid bit also has meaning when applied to a directory. Ordinarily, the group owner of a newly created file is the same as the effective group ID of the process that creates the file. However, when the setgid bit is set on the directory in which a new file is created, the group owner of the newly created file will instead be the group owner of the directory. In addition, Linux will set the setgid bit on directories created within a directory having the setgid bit set.

On systems that support mandatory locking, the setgid bit also has special meaning on nonexecutable files. We discuss its meaning in the context of mandatory locking in Recipe 2.8.

See Also

Recipe 1.3, Recipe 1.4, Recipe 2.8


[1] Saved user IDs may not be available on some very old Unix platforms, but are available on all modern Unixes.

2.2. Understanding the Windows Access Control Model

Problem

You want to understand how access control works on Windows systems.

Solution

Versions of Windows before Windows NT have no access control whatsoever. Windows 95, Windows 98, and Windows ME are all intended to be single-user desktop operating systems and thus have no need for access control. Windows NT, Windows 2000, Windows XP, and Windows Server 2003 all use a system of access control lists (ACLs).

Most users do not understand the Windows access control model and generally regard it as being overly complex. However, it is actually rather straightforward and easy to understand. Unfortunately, from a programmer's perspective, the API for dealing with ACLs is not so easy to deal with.

In Section 2.2.3, we describe the Windows access control model from a high level. We do not provide examples of using the API here, but other recipes throughout the book do provide such examples.

Discussion

All Windows resources, including files, the registry, synchronization primitives (e.g., mutexes and events), and IPC mechanisms (e.g., pipes and mailslots), are accessed through objects, which may be secured using ACLs. Every ACL contains a discretionary access control list (DACL) and asystem access control list (SACL). DACLs determine access rights to an object, and SACLs determine auditing (e.g., logging) policy. In this recipe, we are concerned only with access rights, so we will discuss only DACLs.

A DACL contains zero or more access control entries (ACEs). A DACL with no ACEs, said to be a NULL DACL , is essentially the equivalent of granting full access to everyone, which is never a good idea. A NULL DACL means anyone can do anything to the object. Not only does full access imply the ability to read from or write to the object, it also implies the ability to take ownership of the object or modify its DACL. In the hands of an attacker, the ability to take ownership of the object and modify its DACL can result in denial of service attacks because the object should be accessible but no longer is.

An ACE (an ACL contains one or more ACEs) consists of three primary pieces of information: a security ID (SID), an access right, and a boolean indicator of whether the ACE allows or denies the access right to the entity identified by the ACE's SID. A SID uniquely identifies a user or group on a system. The special SID, known as "Everyone" or "World", identifies all users and groups on the system. All objects support a generic set of access rights, and some objects may define others specific to their type. Table 2-1 lists the generic access rights. Finally, an ACE can either allow or deny an access right.

Table 2-1. Generic access rights supported by all objects

Access right (C constant)

Description

DELETE

The ability to delete the object

READ_CONTROL

The ability to read the object's security descriptor, not including its SACL

SYNCHRONIZE

The ability for a thread to wait for the object to be put into the signaled state; not all objects support this functionality

WRITE_DAC

The ability to modify the object's DACL

WRITE_OWNER

The ability to set the object's owner

GENERIC_READ

The ability to read from or query the object

GENERIC_WRITE

The ability to write to or modify the object

GENERIC_EXECUTE

The ability to execute the object (applies primarily to files)

GENERIC_ALL

Full control

When Windows consults an ACL to verify access to an object, it will always choose the best match. That is, if a deny ACE for "Everyone" is found, and an allow ACE is then found for a specific user that happens to be the current user, Windows will use the allow ACE. For example, suppose that the DACL for a data file contains the following ACEs:

DENY GENERIC_ALL Everyone

This ACE prevents anyone except for the owner of the file from performing any action on the file.

ALLOW GENERIC_WRITE Marketing

Anyone that is a member of the group "Marketing" will be allowed to write to the file because this ACE explicitly allows that access right for that group.

ALLOW GENERIC_READ Everyone

This ACE grants read access to the file to everyone.

All objects are created with an owner. The owner of an object is ordinarily the user who created the object; however, depending on the object's ACL, another user could possibly take ownership of the object. The owner of an object always has full control of the object, regardless of what the object's DACL says. Unfortunately, if an object is not sufficiently protected, an attacker can nefariously take ownership of the object, rendering the rightful owner powerless to counter the attacker.

2.3. Determining Whether a User Has Access to a File on Unix

Problem

Your program is running with extra permissions because its executable has the setuid or setgid bit set. You need to determine whether the user running the program will be able to access a file without the extra privileges granted by setuid or setgid.

Solution

Temporarily drop privileges to the user and group for which access is to be checked. With the process's privileges lowered, perform the access check, then restore privileges to what they were before the check. See Recipe 1.3 for additional discussion of elevated privileges and how to drop and restore them.

Discussion

It is always best to allow the operating system to do the bulk of the work of performing access checks. The only way to do so is to manipulate the privileges under which the process is running. Recipe 1.3 provides implementations for functions that temporarily drop privileges and then restore them again.

When performing access checks on files, you need to be careful to avoid the types of race conditions known as Time of Check, Time of Use (TOCTOU), which are illustrated in Figure 2-1 and Figure 2-2. These race conditions occur when access is checked before opening a file. The most common way for this to occur is to use the access( ) system call to verify access to a file, and then to use open( ) or fopen( ) to open the file if the return from access( ) indicates that access will be granted.

The problem is that between the time the access check via access( ) completes and the time open( ) begins (both system calls are atomic within the operating system kernel), there is a window of vulnerability where an attacker can replace the file that is being operated upon. Let's say that a program uses access( ) to check to see whether an attacker has write permissions to a particular file, as shown in Figure 2-1. If that file is a symbolic link, access( ) will follow it, and report that the attacker does indeed have write permissions for the underlying file. If the attacker can change the symbolic link after the check occurs, but before the program starts using the file, pointing it to a file he couldn't otherwise access, the privileged program will end up opening a file that it shouldn't, as shown in Figure 2-2. The problem is that the program can manipulate either file, and it gets tricked into opening one on behalf of the user that it shouldn't have.

Stage 1 of a TOCTOU race condition: Time of Check

Figure 2-1. Stage 1 of a TOCTOU race condition: Time of Check

Stage 2 of a TOCTOU race condition: Time of Use

Figure 2-2. Stage 2 of a TOCTOU race condition: Time of Use

While such an attack might sound impossible to perform, attackers have many tricks to slow down a program to make exploiting race conditions easier. Plus, even if an attacker can only exploit the race condition every 1,000 times, generally the attack can be automated.

The best approach is to actually have the program take on the identity of the unprivileged user before opening the file. That way, the correct access permission checks will happen automatically when the file is opened. You need not even call access( ). After the file is opened, the program can revert to its privileged state. For example, here's some pseudo-code that opens a file properly, using the spc_drop_privileges( ) and spc_restore_privileges( ) functions from Recipe 1.3:

int fd;

/* Temporarily drop drivileges */

spc_drop_privileges(0);

/* Open the file with the limited privileges */

fd = open("/some/file/that/needs/opening", O_RDWR);

/* Restore privileges */

spc_restore_privileges( );

/* Check the return value from open to see if the file was opened successfully. */

if (fd = = -1) {

perror("open(\"/some/file/that/needs/opening\")");

abort( );

}

There are many other situations where security-critical race conditions occur, particularly in file access. Basically, every time a condition is explicitly checked, one needs to make sure that the result cannot have changed by the time that condition is acted upon.

2.4. Determining Whether a Directory Is Secure

Problem

Your application needs to store sensitive information on disk, and you want to ensure that the directory used cannot be modified by any other entity on the system besides the current user and the administrator. That is, you would like a directory where you can modify the contents at will, without having to worry about future permission checks.

Solution

Check the entire directory tree above the one you intend to use for unsafe permissions. Specifically, you are looking for the ability for users other than the owner and the superuser (the Administrator account on Windows) to modify the directory. On Windows, the required directory traversal cannot be done without introducing race conditions and a significant amount of complex path processing. The best advice we can offer, therefore, is to consider home directories (typically x:\Documents and Settings\User, where x is the boot drive and User is the user's account name) the safest directories. Never consider using temporary directories to store files that may contain sensitive data.

Discussion

Storing sensitive data in files requires extra levels of protection to ensure that the data is not compromised. An often overlooked aspect of protection is ensuring that the directories that contain files (which, in turn, contain sensitive data) are safe from modification.

This may appear to be a simple matter of ensuring that the directory is protected against any other users writing to it, but that is not enough. All the directories in the path must also be protected against any other users writing to them. This means that the same user who will own the file containing the sensitive data also owns the directories, and that the directories are all protected against other users modifying them.

The reason for this is that when a directory is writable by a particular user, that user is able to rename directories and files that reside within that directory. For example, suppose that you want to store sensitive data in a file that will be placed into the directory /home/myhome/stuff/securestuff. If the directory /home/myhome/stuff is writable by another user, that user could rename the directory securestuff to something else. The result would be that your program would no longer be able to find the file containing its sensitive data.

Even if the securestuff directory is owned by the user who owns the file containing the sensitive data, and the permissions on the directory prevent other users from writing to it, the permissions that matter are on the parent directory, /home/myhome/stuff. This same problem exists for every directory in the path, right up to the root directory.

In this recipe we present a function, spc_is_safedir( ) , for checking all of the directories in a path specification on Unix. It traverses the directory tree from the bottom back up to the root, ensuring that only the owner or superuser have write access to each directory.

The spc_is_safedir( ) function requires a single argument specifying the directory to check. The return value from the function is -1 if some kind of error occurs while attempting to verify the safety of the path specification, 0 if the path specification is not safe, or 1 if the path specification is safe.

WARNING

On Unix systems, a process has only one current directory; all threads within a process share the same working directory. The code presented here changes the working directory as it works; therefore, the code is not thread-safe!

#include <sys/types.h>

#include <sys/stat.h>

#include <dirent.h>

#include <fcntl.h>

#include <limits.h>

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

int spc_is_safedir(const char *dir) {

DIR *fd, *start;

int rc = -1;

char new_dir[PATH_MAX + 1];

uid_t uid;

struct stat f, l;

if (!(start = opendir("."))) return -1;

if (lstat(dir, &l) = = -1) {

closedir(start);

return -1;

}

uid = geteuid( );

do {

if (chdir(dir) = = -1) break;

if (!(fd = opendir("."))) break;

if (fstat(dirfd(fd), &f) = = -1) {

closedir(fd);

break;

}

closedir(fd);

if (l.st_mode != f.st_mode || l.st_ino != f.st_ino || l.st_dev != f.st_dev)

break;

if ((f.st_mode & (S_IWOTH | S_IWGRP)) || (f.st_uid && f.st_uid != uid)) {

rc = 0;

break;

}

dir = "..";

if (lstat(dir, &l) = = -1) break;

if (!getcwd(new_dir, PATH_MAX + 1)) break;

} while (new_dir[1]); /* new_dir[0] will always be a slash */

if (!new_dir[1]) rc = 1;

fchdir(dirfd(start));

closedir(start);

return rc;

}

2.5. Erasing Files Securely

Problem

You want to erase a file securely, preventing recovery of any data via "undelete" tools or any inspection of the disk for data that has been left behind.

Solution

Write over the data in the file multiple times, varying the data written each time. You should write both random and patterned data for maximum effectiveness.

Discussion

WARNING

It is extremely difficult, if not outright impossible, to guarantee that the contents of a file are completely unrecoverable on modern operating systems that offer logging filesystems, virtual memory, and other such features.

Securely deleting files from disk is not as simple as issuing a system call to delete the file from the filesystem. The first problem is that most delete operations do not do anything to the data; they merely delete any underlying metadata that the filesystem uses to associate the file contents with the filename. The storage space where the actual data is stored is then marked free and will be reclaimed whenever the filesystem needs that space.

The result is that to truly erase the data, you need to overwrite it with nonsense before the filesystem delete operation is performed. Many times, this overwriting is implemented by simply zeroing all the bytes in the file. While this will certainly erase the file from the perspective of most conventional utilities, the fact that most data is stored on magnetic media makes this more complicated.

More sophisticated tools can analyze the actual media and reveal the data that was previously stored on it. This type of data recovery has a limit, however. If the data is sufficiently overwritten on the media, it does become unrecoverable, masked by the new data that has overwritten it. A variety of factors, such as the type of data written and the characteristics of the media, determine the point at which the interesting data becomes unrecoverable.

A technique developed by Peter Gutmann provides an algorithm involving multiple passes of data written to the disk to delete a file securely. The passes involve both specific patterns and random data written to the disk. The paper detailing this technique is available fromhttp://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html.

Unfortunately, many factors also work to thwart the feasibility of securely wiping the contents of a file. Many modern operating systems employ complex filesystems that may cause several copies of any given file to exist in some form at various different locations on the media. Other modern operating system features such as virtual memory often work to defeat the goal of securely obliterating any traces of sensitive data.

One of the worst things that can happen is that filesystem caching will turn multiple writes into a single write operation. On some platforms, calling fsync( ) on the file after one pass will generally cause the filesystem to flush the contents of the file to disk. But on some platforms that's not necessarily sufficient. Doing a better job requires knowing about the operating system on which your code is running. For example, you might be able to wait 10 minutes between passes, and ensure that the cached file has been written to disk at least once in that time frame. Below, we provide an implementation of Peter Gutmann's secure file-wiping algorithm, assuming fsync( ) is enough.

TIP

On Windows XP and Windows Server 2003, you can use the cipher command with the /w flag to securely wipe unused portions of NTFS filesystems.

We provide three functions:

spc_fd_wipe( )

Overwrites the contents of a file identified by the specified file descriptor in accordance with Gutmann's algorithm. If an error occurs while performing the wipe operation, the return value is -1; otherwise, a successful operation returns zero.

spc_file_wipe( )

A wrapper around the first function, which uses a FILE object instead of a file descriptor. If an error occurs while performing the wipe operation, the return value is -1; otherwise, a successful operation returns zero.

SpcWipeFile( )

A Windows-specific function that uses the Win32 API for file access. It requires an open file handle as its only argument and returns a boolean indicating success or failure.

Note that for all three functions, the file descriptor, FILE object, or file handle passed as an argument must be open with write access to the file to be wiped; otherwise, the wiping functions will fail. As written, these functions will probably not work very well on media other than disk because they are constantly seeking back to the beginning of the file. Another issue that may arise is filesystem caching. All the writes made to the file may not actually be written to the physical media.

#include <limits.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <unistd.h>

#include <errno.h>

#include <stdio.h>

#include <string.h>

#define SPC_WIPE_BUFSIZE 4096

static int write_data(int fd, const void *buf, size_t nbytes) {

size_t towrite, written = 0;

ssize_t result;

do {

if (nbytes - written > SSIZE_MAX) towrite = SSIZE_MAX;

else towrite = nbytes - written;

if ((result = write(fd, (const char *)buf + written, towrite)) >= 0)

written += result;

else if (errno != EINTR) return 0;

} while (written < nbytes);

return 1;

}

static int random_pass(int fd, size_t nbytes)

{

size_t towrite;

unsigned char buf[SPC_WIPE_BUFSIZE];

if (lseek(fd, 0, SEEK_SET) != 0) return -1;

while (nbytes > 0) {

towrite = (nbytes > sizeof(buf) ? sizeof(buf) : nbytes);

spc_rand(buf, towrite);

if (!write_data(fd, buf, towrite)) return -1;

nbytes -= towrite;

}

fsync(fd);

return 0;

}

static int pattern_pass(int fd, unsigned char *buf, size_t bufsz, size_t filesz) {

size_t towrite;

if (!bufsz || lseek(fd, 0, SEEK_SET) != 0) return -1;

while (filesz > 0) {

towrite = (filesz > bufsz ? bufsz : filesz);

if (!write_data(fd, buf, towrite)) return -1;

filesz -= towrite;

}

fsync(fd);

return 0;

}

int spc_fd_wipe(int fd) {

int count, i, pass, patternsz;

struct stat st;

unsigned char buf[SPC_WIPE_BUFSIZE], *pattern;

static unsigned char single_pats[16] = {

0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,

0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff

};

static unsigned char triple_pats[6][3] = {

{ 0x92, 0x49, 0x24 }, { 0x49, 0x24, 0x92 }, { 0x24, 0x92, 0x49 },

{ 0x6d, 0xb6, 0xdb }, { 0xb6, 0xdb, 0x6d }, { 0xdb, 0x6d, 0xb6 }

};

if (fstat(fd, &st) = = -1) return -1;

if (!st.st_size) return 0;

for (pass = 0; pass < 4; pass++)

if (random_pass(fd, st.st_size) = = -1) return -1;

memset(buf, single_pats[5], sizeof(buf));

if (pattern_pass(fd, buf, sizeof(buf), st.st_size) = = -1) return -1;

memset(buf, single_pats[10], sizeof(buf));

if (pattern_pass(fd, buf, sizeof(buf), st.st_size) = = -1) return -1;

patternsz = sizeof(triple_pats[0]);

for (pass = 0; pass < 3; pass++) {

pattern = triple_pats[pass];

count = sizeof(buf) / patternsz;

for (i = 0; i < count; i++)

memcpy(buf + (i * patternsz), pattern, patternsz);

if (pattern_pass(fd, buf, patternsz * count, st.st_size) = = -1) return -1;

}

for (pass = 0; pass < sizeof(single_pats); pass++) {

memset(buf, single_pats[pass], sizeof(buf));

if (pattern_pass(fd, buf, sizeof(buf), st.st_size) = = -1) return -1;

}

for (pass = 0; pass < sizeof(triple_pats) / patternsz; pass++) {

pattern = triple_pats[pass];

count = sizeof(buf) / patternsz;

for (i = 0; i < count; i++)

memcpy(buf + (i * patternsz), pattern, patternsz);

if (pattern_pass(fd, buf, patternsz * count, st.st_size) = = -1) return -1;

}

for (pass = 0; pass < 4; pass++)

if (random_pass(fd, st.st_size) = = -1) return -1;

return 0;

}

int spc_file_wipe(FILE *f) {

return spc_fd_wipe(fileno(f));

}

The Unix implementations should work on Windows systems using the standard C runtime API; however, it is rare that the standard C runtime API is used on Windows. The following code implements SpcWipeFile( ), which is virtually identical to the standard C version except that it uses only Win32 APIs for file access.

#include <windows.h>

#include <wincrypt.h>

#define SPC_WIPE_BUFSIZE 4096

static BOOL RandomPass(HANDLE hFile, HCRYPTPROV hProvider, DWORD dwFileSize)

{

BYTE pbBuffer[SPC_WIPE_BUFSIZE];

DWORD cbBuffer, cbTotalWritten, cbWritten;

if (SetFilePointer(hFile, 0, 0, FILE_BEGIN) = = 0xFFFFFFFF) return FALSE;

while (dwFileSize > 0) {

cbBuffer = (dwFileSize > sizeof(pbBuffer) ? sizeof(pbBuffer) : dwFileSize);

if (!CryptGenRandom(hProvider, cbBuffer, pbBuffer)) return FALSE;

for (cbTotalWritten = 0; cbBuffer > 0; cbTotalWritten += cbWritten)

if (!WriteFile(hFile, pbBuffer + cbTotalWritten, cbBuffer - cbTotalWritten,

&cbWritten, 0)) return FALSE;

dwFileSize -= cbTotalWritten;

}

return TRUE;

}

static BOOL PatternPass(HANDLE hFile, BYTE *pbBuffer, DWORD cbBuffer, DWORD dwFileSize) {

DWORD cbTotalWritten, cbWrite, cbWritten;

if (!cbBuffer || SetFilePointer(hFile, 0, 0, FILE_BEGIN) = = 0xFFFFFFFF) return FALSE;

while (dwFileSize > 0) {

cbWrite = (dwFileSize > cbBuffer ? cbBuffer : dwFileSize);

for (cbTotalWritten = 0; cbWrite > 0; cbTotalWritten += cbWritten)

if (!WriteFile(hFile, pbBuffer + cbTotalWritten, cbWrite - cbTotalWritten,

&cbWritten, 0)) return FALSE;

dwFileSize -= cbTotalWritten;

}

return TRUE;

}

BOOL SpcWipeFile(HANDLE hFile) {

BYTE pbBuffer[SPC_WIPE_BUFSIZE];

DWORD dwCount, dwFileSize, dwIndex, dwPass;

HCRYPTPROV hProvider;

static BYTE pbSinglePats[16] = {

0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,

0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff

};

static BYTE pbTriplePats[6][3] = {

{ 0x92, 0x49, 0x24 }, { 0x49, 0x24, 0x92 }, { 0x24, 0x92, 0x49 },

{ 0x6d, 0xb6, 0xdb }, { 0xb6, 0xdb, 0x6d }, { 0xdb, 0x6d, 0xb6 }

};

static DWORD cbPattern = sizeof(pbTriplePats[0]);

if ((dwFileSize = GetFileSize(hFile, 0)) = = INVALID_FILE_SIZE) return FALSE;

if (!dwFileSize) return TRUE;

if (!CryptAcquireContext(&hProvider, 0, 0, 0, CRYPT_VERIFYCONTEXT))

return FALSE;

for (dwPass = 0; dwPass < 4; dwPass++)

if (!RandomPass(hFile, hProvider, dwFileSize)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

memset(pbBuffer, pbSinglePats[5], sizeof(pbBuffer));

if (!PatternPass(hFile, pbBuffer, sizeof(pbBuffer), dwFileSize)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

memset(pbBuffer, pbSinglePats[10], sizeof(pbBuffer));

if (!PatternPass(hFile, pbBuffer, sizeof(pbBuffer), dwFileSize)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

cbPattern = sizeof(pbTriplePats[0]);

for (dwPass = 0; dwPass < 3; dwPass++) {

dwCount = sizeof(pbBuffer) / cbPattern;

for (dwIndex = 0; dwIndex < dwCount; dwIndex++)

CopyMemory(pbBuffer + (dwIndex * cbPattern), pbTriplePats[dwPass],

cbPattern);

if (!PatternPass(hFile, pbBuffer, cbPattern * dwCount, dwFileSize)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

}

for (dwPass = 0; dwPass < sizeof(pbSinglePats); dwPass++) {

memset(pbBuffer, pbSinglePats[dwPass], sizeof(pbBuffer));

if (!PatternPass(hFile, pbBuffer, sizeof(pbBuffer), dwFileSize)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

}

for (dwPass = 0; dwPass < sizeof(pbTriplePats) / cbPattern; dwPass++) {

dwCount = sizeof(pbBuffer) / cbPattern;

for (dwIndex = 0; dwIndex < dwCount; dwIndex++)

CopyMemory(pbBuffer + (dwIndex * cbPattern), pbTriplePats[dwPass],

cbPattern);

if (!PatternPass(hFile, pbBuffer, cbPattern * dwCount, dwFileSize)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

}

for (dwPass = 0; dwPass < 4; dwPass++)

if (!RandomPass(hFile, hProvider, dwFileSize)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

CryptReleaseContext(hProvider, 0);

return TRUE;

}

See Also

"Secure Deletion of Data from Magnetic and Solid-State Memory" by Peter Gutmann: http://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html

2.6. Accessing File Information Securely

Problem

Y ou need to access information about a file, such as its size or last modification date. In doing so, you want to avoid the possibility of race conditions.

Solution

Use a secure directory, as described in Recipe 2.4. Alternatively, open the file and query the needed information using the file handle. Do not use functions that operate on the name of the file, especially if multiple queries are required for the same file or if you intend to open it based on the information obtained from queries. Operating on filenames introduces the possibility of race conditions because filenames can change between calls.

On Unix, use the fstat( ) function instead of the stat( ) function. Both functions return the same information, but fstat( ) uses an open file descriptor while stat( ) uses a filename. Doing so removes the possibility of a race condition, because the file to which the file descriptor points can never change unless you reopen the file descriptor. When operating on just the filename, there is no guarantee that the underlying file pointed to by the filename remains the same after the call to stat( ).

On Windows, use the function GetFileInformationByHandle( ) instead of functions like FindFirstFile( ) or FindFirstFileEx( ). As with fstat( ) versus stat( ) on Unix (which are also available on Windows if you're using the C runtime API), the primary difference between these functions is that one uses a file handle while the others use filenames. If the only information you need is the size of the file, you can use GetFileSize( ) instead of GetFileInformationByHandle( ).

Discussion

Accessing file information using filenames can lead to race conditions, particularly if multiple queries are necessary or if you intend to open the file depending on information previously obtained. In particular, if symbolic links are involved, an attacker could potentially change the file to which the link points between queries or between the time information is queried and the time the file is actually opened. This type of race condition, known as a Time of Check, Time of Use (TOCTOU) race condition, was also discussed in Recipe 2.3.

In most cases, when you need information about a file, such as its size, you also have some intention of opening the file and using it in some way. For example, if you're checking to see whether a file exists before trying to create it, you might think to use stat( ) or FindFirstFile( ) first, and if the function fails with an error indicating the file does not exist, create the file with creat( ) or CreateFile( ). A better solution is to use open( ) with the O_CREAT and O_EXCL flags, or to use CreateFile( ) with CREATE_NEW specified as the creation disposition.

See Also

Recipe 2.3

2.7. Restricting Access Permissions for New Files on Unix

Problem

You want to restrict the initial access permissions assigned to a file created by your program.

Solution

On Unix, the operating system stores a value known as the umask for each process it uses when creating new files on behalf of the process. The umask is used to disable permission bits that may be specified by the system call used to create files.

Discussion

WARNING

Remember that umasks apply only on file or directory creation. Calls to chmod( ) and fchmod( ) are not modified by umask settings.

When a process creates a new file, it specifies the access permissions to assign the new file as a parameter to the system call that creates the file. The operating system modifies the access permissions by computing the intersection of the inverse of the umask and the permissions requested by the process. The access permission bits that remain after the intersection is computed are what the operating system actually uses for the new file. In other words, in the following example code, if the variable requested_permissions contained the permissions passed to the operating system to create a new file, the variable actual_permissions would be the actual permissions that the operating system would use to create the file.

requested_permissions = 0666;

actual_permissions = requested_permissions & ~umask( );

A process inherits the value of its umask from its parent process when the process is created. Normally, the shell sets a default umask of either 022 (disable group- and world-writable bits) or 02 (disable world-writable bits) when a user logs in, but users have free reign to change the umask as they want. Many users are not even aware of the existence of umasks, never mind how to set them appropriately. Therefore, the umask value as set by the user should never be trusted to be appropriate.

When using the open( ) system call to create a new file, you can force more restrictive permissions to be used than what the user's umask might allow, but the only way to create a file with less restrictive permissions is either to modify the umask before creating the file or to use fchmod( ) to change the permissions after the file is created.

In most cases, you'll be attempting to loosen restrictions, but consider what happens when fopen( ) is used to create a new file. The fopen( ) function provides no way to specify the permissions to use for the new file, and it always uses 0666, which grants read and write access to the owning user, the owning group, and everyone else. Again, the only way to modify this behavior is either to set the umask before calling fopen( ) or to use fchmod( ) after the file is created.

Using fchmod( ) to change the permissions of a file after it is created is not a good idea because it introduces a race condition. Between the time the file is created and the time the permissions are modified, an attacker could possibly gain unauthorized access to the file. The proper solution is therefore to modify the umask before creating the file.

Properly using umasks in your program can be a bit complicated, but here are some general guidelines:

§ If you are creating files that contain sensitive data, always create them readable and writable by only the file owner, and deny access to group members and all other users.

§ Be aware that files that do not contain sensitive data may be readable by other users on the system. If the user wants to stop this behavior, the umask can be set appropriately before starting your program.

§ Avoid setting execute permissions on files, especially group and world execute. If your program generates files that are meant to be executable, set the execute bit only for the file owner.

§ Create directories that may contain files used to store sensitive information such that only the owner of the directory has read, write, and execute permissions for the directory. This allows only the owner of the directory to enter the directory or view or change its contents, but no other users can view or otherwise access the directory. (See the discussion of secure directories in Recipe 2.4 for more information on the importance of this requirement.)

§ Create directories that are not intended to store sensitive files such that the owner has read, write, and execute permissions, while group members and everyone else has only read and execute permissions. If the user wants to stop this behavior, the umask can be set appropriately before starting your program.

§ Do not rely on setting the umask to a "secure" value once at the beginning of the program and then calling all file or directory creation functions with overly permissive file modes. Explicitly set the mode of the file at the point of creation. There are two reasons to do this. First, it makes the code clear; your intent concerning permissions is obvious. Second, if an attacker managed to somehow reset the umask between your adjustment of the umask and any of your file creation calls, you could potentially create sensitive files with wide-open permissions.

Modifying the umask programmatically is a simple matter of calling the function umask( ) with the new mask. The return value will be the old umask value. The standard header file sys/stat.h prototypes the umask( ) function, and it also contains definitions for a sizable set of macros that map to the various permission bits. Table 2-2 lists the macros, their values in octal, and the permission bit or bits to which each one corresponds.

Table 2-2. Macros for permission bits and their octal values

Macro

Octal value

Permission bit(s)

S_IRWXU

0700

Owner read, write, execute

S_IRUSR

0400

Owner read

S_IWUSR

0200

Owner write

S_IXUSR

0100

Owner execute

S_IRWXG

0070

Group read, write, execute

S_IRGRP

0040

Group read

S_IWGRP

0020

Group write

S_IXGRP

0010

Group execute

S_IRWXO

0007

Other/world read, write, execute

S_IROTH

0004

Other/world read

S_IWOTH

0002

Other/world write

S_IXOTH

0001

Other/world execute

umasks are a useful tool for users, allowing them to limit the amount of access others get to their files. Your program should make every attempt to honor the users' wishes in this regard, but if extra security is required for files that your application generates, you should always explicitly set this permission yourself.

See Also

Recipe 2.4

2.8. Locking Files

Problem

You want to lock files (or portions of them) to prevent two or more processes from accessing them simultaneously.

Solution

Two basic types of locks exist: advisory and mandatory. Unix supports both advisory and, to an extremely limited extent, mandatory locks, while Windows supports only mandatory locks.

Discussion

In the following sections, we will look at the different issues for Unix and Windows.

Locking files on Unix

All modern Unix variants support advisory locks. An advisory lock is a lock in which the operating system does not enforce the lock. Instead, programs sharing the same file must cooperate with each other to ensure that locks are properly observed. From a security perspective, advisory locks are of little use because any program is free to perform any action on a file regardless of the state of any advisory locks that other programs may hold on the file.

Support for mandatory locks varies greatly from one Unix variant to another. Both Linux and Solaris support mandatory locks, but Darwin, FreeBSD, NetBSD, and OpenBSD do not, even though they export the interface used by Linux and Solaris to support them. On such systems, this interface creates advisory locks.

Support for mandatory locking does not extend to NFS. In other words, both Linux and Solaris are capable only of using mandatory locks on local filesystems. Further, Linux requires that filesystems be mounted with support for mandatory locking, which is disabled by default. In the end, Solaris is really the only Unix variant on which you can reasonably expect mandatory locking to work, and even then, relying on mandatory locks is like playing with fire.

As if the story for mandatory locking on Unix were not bad enough already, it gets worse. To be able to use mandatory locks on a file, the file must have the setgid bit enabled and the group execute bit disabled in its permissions. Even if a process holds a mandatory lock on a file, another process may remove the setgid bit from the file's permissions, which effectively turns the mandatory lock into an advisory lock!

Essentially, there is no such thing as a mandatory lock on Unix.

Just to add more fuel to the fire, neither Solaris nor Linux fully or properly implement the System V defined semantics for mandatory locks, and both systems differ in where they stray from the System V definitions. The details of the differences are not important here. We strongly recommend that you avoid the Unix mandatory lock debacle altogether. If you want to use advisory locking on Unix, then we recommend using a standalone lock file, as described in Recipe 2.9.

Locking files on Windows

Where Unix falls flat on its face with respect to supporting file locking, Windows gets it right. Windows supports only mandatory file locks, and it fully enforces them. If a process has a lock on a file or a portion of a file, another process cannot mistakenly or maliciously steal that lock.

Windows provides four functions for locking and unlocking files. Two functions, LockFile( ) and LockFileEx( ) , are provided for engaging locks, and two functions, UnlockFile( ) and UnlockFileEx( ) , are provided for removing them.

Neither LockFile( ) nor UnlockFile( ) will return until the lock can be successfully obtained or released, respectively. LockFileEx( ) and UnlockFileEx( ), however, can be called in such a way that they will always return immediately, either returning failure or signalling an event object when the requested operation completes.

Locks can be placed on a file in its entirety or on a portion of a file. A single file may have multiple locks owned by multiple processes so long as none of the locks overlap. When removing a lock, you must specify the exact portion of the file that was locked. For example, two locks covering contiguous portions of a file may not be removed with a single unlock operation that spans the two locks.

WARNING

When a lock is held on a file, closing the file does not necessarily remove the lock. The behavior is actually undefined and may vary across different filesystems and versions of Windows. Always make sure to remove any locks on a file before closing it.

There are two types of locks on Windows:

Shared lock

This type of lock allows other processes to read from the locked portion of the file, while denying all processes—including the process that obtained the lock—permission to write to the locked portion of the file.

Exclusive lock

This type of lock denies other processes both read and write access to the locked portion of the file, while allowing the locking process to read or write to the locked portion of the file.

Using LockFile( ) to obtain a lock always obtains an exclusive lock. However, LockFileEx( ) obtains a shared lock unless the flag LOCKFILE_EXCLUSIVE_LOCK is specified.

Here are the signatures for LockFile and UnlockFile( ):

BOOL LockFile(HANDLE hFile, DWORD dwFileOffsetLow,

DWORD dwFileOffsetHigh, DWORD nNumberOfBytesToLockLow,

DWORD nNumberOfBytesToLockHigh);

BOOL UnlockFile(HANDLE hFile, DWORD dwFileOffsetLow,

DWORD dwFileOffsetHigh, DWORD nNumberOfBytesToUnlockLow,

DWORD nNumberOfBytesToUnlockHigh);

2.9. Synchronizing Resource Access Across Processes on Unix

Problem

You want to ensure that two processes cannot simultaneously access the same resource, such as a segment of shared memory.

Solution

Use a lock file to signal that you are accessing the resource.

Discussion

Using a lock file to synchronize access to shared resources is not as simple as it sounds. Suppose that your program creates a lock file and then crashes. If this happens, the lock file will remain, and your program (as well as any other program that attempted to obtain the lock) will fail until someone manually removes the lock file. Obviously, this is undesirable. The solution is to store the process ID of the process holding the lock in the lock file. Other processes attempting to obtain the lock can then test to see whether the process holding the lock still exists. If it does not, the lock file is stale, it is safe to remove, and you can make another attempt to obtain the lock.

Unfortunately, this solution is still not a perfect one. What happens if another process is assigned the same ID as the one stored in the stale lock file? The answer to this question is simply that no process can obtain the lock until the process with the stale ID terminates or someone manually removes the lock file. Fortunately, this case should not be encountered frequently.

As a result of solving the stale lock problem, a new problem arises: there is now a race condition between the time the check for the existence of the process holding the lock is performed and the time the lock file is removed. The solution to this problem is to attempt to reopen the lock file after writing the new one to make sure that the process ID in the lock file is the same as the locking process's ID. If it is, the lock is successfully obtained.

The function presented below, spc_lock_file( ) , requires a single argument: the name of the file to be used as the lock file. You must store the lock file in a "safe" directory (see Recipe 2.4) on a local filesystem. Network filesystems—versions of NFS older than Version 3 in particular—may not necessarily support the O_EXCL flag to open( ) . Further, because the ID of the process holding the lock is stored in the lock file and process IDs are not shared across machines, testing for the presence of the process holding the lock would be unreliable at best if the lock file were stored on a network filesystem.

Three attempts are made to obtain the lock, with a pause of one second between attempts. If the lock cannot be obtained, the return value from the function is 0. If some kind of error occurs in attempting to obtain the lock, the return value is -1. If the lock is successfully obtained, the return value is 1.

#include <sys/types.h>

#include <unistd.h>

#include <stdlib.h>

#include <fcntl.h>

#include <sys/stat.h>

#include <errno.h>

#include <limits.h>

#include <signal.h>

static int read_data(int fd, void *buf, size_t nbytes) {

size_t toread, nread = 0;

ssize_t result;

do {

if (nbytes - nread > SSIZE_MAX) toread = SSIZE_MAX;

else toread = nbytes - nread;

if ((result = read(fd, (char *)buf + nread, toread)) >= 0)

nread += result;

else if (errno != EINTR) return 0;

} while (nread < nbytes);

return 1;

}

static int write_data(int fd, const void *buf, size_t nbytes) {

size_t towrite, written = 0;

ssize_t result;

do {

if (nbytes - written > SSIZE_MAX) towrite = SSIZE_MAX;

else towrite = nbytes - written;

if ((result = write(fd, (const char *)buf + written, towrite)) >= 0)

written += result;

else if (errno != EINTR) return 0;

} while (written < nbytes);

return 1;

}

The two functions read_data( ) and write_data( ) are helper functions that ensure that all the requested data is read or written. If the system calls for reading or writing are interrupted by a signal, they are retried. Because such a small amount of data is being read and written, the data should all be written atomically, but all the data may not be read or written in a single call. These helper functions also handle this case.

int spc_lock_file(const char *lfpath) {

int attempt, fd, result;

pid_t pid;

/* Try three times, if we fail that many times, we lose */

for (attempt = 0; attempt < 3; attempt++) {

if ((fd = open(lfpath, O_RDWR | O_CREAT | O_EXCL, S_IRWXU)) = = -1) {

if (errno != EEXIST) return -1;

if ((fd = open(lfpath, O_RDONLY)) = = -1) return -1;

result = read_data(fd, &pid, sizeof(pid));

close(fd);

if (result) {

if (pid = = getpid( )) return 1;

if (kill(pid, 0) = = -1) {

if (errno != ESRCH) return -1;

attempt--;

unlink(lfpath);

continue;

}

}

sleep(1);

continue;

}

pid = getpid( );

if (!write_data(fd, &pid, sizeof(pid))) {

close(fd);

return -1;

}

close(fd);

attempt--;

}

/* If we've made it to here, three attempts have been made and the lock could

* not be obtained. Return an error code indicating failure to obtain the

* requested lock.

*/

return 0;

}

The first step in attempting to obtain the lock is to try to create the lock file. If this succeeds, the caller's process ID is written to the file, the file is closed, and the loop is executed again. The loop counter is decremented first to ensure that at least one more iteration will always occur. The next time through the loop, creating the file should fail but won't necessarily do so, because another process was attempting to get the lock at the same time from a stale process and deleted the lock file out from under this process. If this happens, the whole process begins again.

If the lock file cannot be created, the lock file is opened for reading, and the ID of the process holding the lock is read from the file. The read is blocking, so if another process has begun to write out its ID, the read will block until the other process is done. Another race condition here could be avoided by performing a non-blocking read in a loop until all the data is read. A timeout could be applied to the read operation to cause the incomplete lock to be treated as stale. This race condition will only occur if a process creates the lock file without writing any data to it. This could be caused by an attacker, or it could occur because the process is terminated at precisely the right time so that it doesn't get the chance to write its ID to the lock file.

Once the process ID is read from the lock file, an attempt to send the process a signal of 0 is made. If the signal cannot be sent because the process does not exist, the call to kill( ) will return failure, and errno will be set to ESRCH. If this happens, the lock file is stale, and it can be removed. This is where the race condition discussed earlier occurs. The lock file is removed, the attempt counter is decremented, and the loop is restarted.

Between the time that kill( ) returns failure with an ESRCH error code and the time that unlink( ) is called to remove the lock file, another process could successfully delete the lock file and begin creating a new one. If this happens, the process will successfully write its process ID to the now deleted lock file and assume that it has the lock. It will not have the lock, though, because this process will have deleted the lock file the other process was creating. For this reason, after the lock file is created, the process must attempt to read the lock file and compare process IDs. If the process ID in the lock file is the same as the process making the comparison, the lock was successfully obtained.

See Also

Recipe 2.4

2.10. Synchronizing Resource Access Across Processes on Windows

Problem

You want to ensure that two processes cannot simultaneously access the same resource.

Solution

Use a named mutex (mutually exclusive lock) to synchronize access to the resource.

Discussion

Coordinating access to a shared resource between multiple processes on Windows is much simpler and much more elegant than it is on Unix. For maximum portability on Unix, you must use a lock file and make sure to avoid a number of possible race conditions to make lock files work properly. On Windows, however, the use of named mutexes solves all the problems Unix has without introducing new ones.

A named mutex is a synchronization object that works by allowing only a single thread to acquire a lock at any given time. Mutexes can also exist without a name, in which case they are considered anonymous. Access to an anonymous mutex can only be obtained by somehow acquiring a handle to the object from the thread that created it. Anonymous mutexes are of no use to us in this recipe, so we won't discuss them further.

Mutexes have a namespace much like that of a filesystem. The mutex namespace is separate from namespaces used by all other objects. If two or more applications agree on a name for a mutex, access to the mutex can always be obtained to use it for synchronizing access to a shared resource.

A mutex is created with a call to the CreateMutex( ) function. You will find it particularly useful in this recipe that the mutex is created and a handle returned, or, if the mutex already exists, a handle to the existing mutex is returned.

Once we have a handle to the mutex that will be used for synchronization, using it is a simple matter of waiting for the mutex to enter the signaled state. When it does, we obtain the lock, and other processes wait for us to release it. When we are finished using the resource, we simply release the lock, which places the mutex into the signaled state.

If our program terminates abnormally while it holds the lock on the resource, the lock is released, and the return from WaitForSingleObject( ) in the next process to obtain the lock is WAIT_ABANDONED. We do not check for this condition in our code because the code is intended to be used in such a way that abandoning the lock will not have any adverse effects. This is essentially the same type of behavior as that in the Unix lock file code from Recipe 2.9, where it attempts to break the lock if the process holding it terminates unexpectedly.

To obtain a lock, call SpcLockResource( ) with the name of the lock. If the lock is successfully obtained, the return will be a handle to the lock; otherwise, the return will be NULL, and GetLastError( ) can be used to determine what went wrong. When you're done with the lock, release it by calling SpcUnlockResource( ) with the handle returned by SpcLockResource( ).

#include <windows.h>

HANDLE SpcLockResource(LPCTSTR lpName) {

HANDLE hResourceLock;

if (!lpName) {

SetLastError(ERROR_INVALID_PARAMETER);

return 0;

}

if (!(hResourceLock = CreateMutex(0, FALSE, lpName))) return 0;

if (WaitForSingleObject(hResourceLock, INFINITE) = = WAIT_FAILED) {

CloseHandle(hResourceLock);

return 0;

}

return hResourceLock;

}

BOOL SpcUnlockResource(HANDLE hResourceLock) {

if (!ReleaseMutex(hResourceLock)) return FALSE;

CloseHandle(hResourceLock);

return TRUE;

}

See Also

Recipe 2.9

2.11. Creating Files for Temporary Use

Problem

You need to create a file to use as scratch space that may contain sensitive data.

Solution

Generate a random filename and attempt to create the file, failing if the file already exists. If the file cannot be created because it already exists, repeat the process until it succeeds. If creating the file fails for any other reason, abort the process.

Discussion

WARNING

When creating temporary files, you should consider using a known-safe directory to store them, as described in Recipe 2.4.

The need for temporary files is common. More often than not, other processes have no need to access the temporary files you create, and especially if the files contain sensitive data, it is best to do everything possible to ensure that other processes cannot access them. It is also important that temporary files do not remain on the filesystem any longer than necessary. If the program creating temporary files terminates unexpectedly before it cleans up the files, temporary directories often become littered with files of no interest or value to anyone or anything. Worse, if the temporary files contain sensitive data, they are suddenly both interesting and valuable to an attacker.

Temporary files on Unix

The best solution for creating a temporary file on Unix is to use the mkstemp( ) function in the standard C runtime library. This function generates a random filename,[2] attempts to create it, and repeats the whole process until it is successful, thus guaranteeing that a unique file is created. The file created by mkstemp( ) will be readable and writable by the owner, but not by anyone else.

To help further ensure that the file cannot be accessed by any other process, and to be sure that the file will not be left behind by your program if it should terminate unexpectedly before being able to delete it, the file can be deleted by name while it is open immediately after mkstemp( ) returns. Even though the file has been deleted, you will still be able to read from and write to it because there is a valid descriptor for the file. No other process will be able to open the file because a name will no longer be associated with it. Once the last open descriptor to the file is closed, the file will no longer be accessible.

WARNING

Between the time that a file is created with mkstemp( ) and the time that unlink( ) is called to delete the file, a window of opportunity exists where an attacker could open the file before it can be deleted.

The mkstemp( ) function works by specifying a template from which a random filename can be generated. From the end of the template, "X" characters are replaced with random characters. The template is modified in place, so the specified buffer must be writable. The return value frommkstemp( ) is -1 if an error occurs; otherwise, it is the file descriptor to the file that was created.

Temporary files on Windows

The Win32 API does not contain a functional equivalent of the standard C mkstemp( ) function. The Microsoft C Runtime implementation does not even provide support for the function, although it does provide an implementation of mktemp( ) . However, we strongly advise against using that function on either Unix or Windows.

The Win32 API does provide a function, GetTempFileName( ) , that will generate a temporary filename, but that is all that it does; it does not open the file for you. Further, if asked to generate a unique name itself, it will use the system time, which is highly predictable.

Instead, we recommend using GetTempPath( ) to obtain the current user's setting for the location to place temporary files, and generating your own random filename using CryptoAPI or some other cryptographically strong pseudo-random number generator. The code presented here uses thespc_rand_range( ) function from Recipe 11.11. Refer to Chapter 11 for possible implementations of random number generators.

The function SpcMakeTempFile( ) repeatedly generates a random temporary filename using a cryptographically strong pseudo-random number generator and attempts to create the file. The generated filename contains an absolute path specification to the user's temporary files directory. If successful, the file is created, inheriting access permissions from that directory, which ordinarily will prevent users other than the Administrator and the owner from gaining access to it. If SpcMakeTempFile( ) is unable to create the file, the process begins anew. SpcMakeTempFile( ) will not return until a file can be successfully created or some kind of fatal error occurs.

As arguments, SpcMakeTempFile( ) requires a preallocated writable buffer and the size of that buffer in characters. The buffer will contain the filename used to successfully create the temporary file, and the return value from the function will be a handle to the open file. If an error occurs, the return value will be INVALID_HANDLE_VALUE, and GetLastError( ) can be used to obtain more detailed error information.

#include <windows.h>

static LPTSTR lpszFilenameCharacters = TEXT("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");

static BOOL MakeTempFilename(LPTSTR lpszBuffer, DWORD dwBuffer) {

int i;

DWORD dwCharacterRange, dwTempPathLength;

TCHAR cCharacter;

dwTempPathLength = GetTempPath(dwBuffer, lpszBuffer);

if (!dwTempPathLength) return FALSE;

if (++dwTempPathLength > dwBuffer || dwBuffer - dwTempPathLength < 12) {

SetLastError(ERROR_INSUFFICIENT_BUFFER);

return FALSE;

}

dwCharacterRange = lstrlen(lpszFilenameCharacters) - 1;

for (i = 0; i < 8; i++) {

cCharacter = lpszFilenameCharacters[spc_rand_range(0, dwCharacterRange)];

lpszBuffer[dwTempPathLength++ - 1] = cCharacter;

}

lpszBuffer[dwTempPathLength++ - 1] = '.';

lpszBuffer[dwTempPathLength++ - 1] = 'T';

lpszBuffer[dwTempPathLength++ - 1] = 'M';

lpszBuffer[dwTempPathLength++ - 1] = 'P';

lpszBuffer[dwTempPathLength++ - 1] = 0;

return TRUE;

}

HANDLE SpcMakeTempFile(LPTSTR lpszBuffer, DWORD dwBuffer) {

HANDLE hFile;

do {

if (!MakeTempFilename(lpszBuffer, dwBuffer)) {

hFile = INVALID_HANDLE_VALUE;

break;

}

hFile = CreateFile(lpszBuffer, GENERIC_READ | GENERIC_WRITE,

FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,

0, CREATE_NEW,

FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, 0);

if (hFile = = INVALID_HANDLE_VALUE && GetLastError( ) != ERROR_ALREADY_EXISTS)

break;

} while (hFile = = INVALID_HANDLE_VALUE);

return hFile;

}

See Also

Recipe 2.4, Recipe 11.11


[2] The filename may not be strongly random. An attacker might be able to predict the filename, but that is generally okay.

2.12. Restricting Filesystem Access on Unix

Problem

You want to restrict your program's ability to access important parts of the filesystem.

Solution

Unix systems provide a system call known as chroot( ) that will restrict the process's access to the filesystem. Specifically, chroot( ) alters a process's perception of the filesystem by changing its root directory, which effectively prevents the process from accessing any part of the filesystem above the new root directory.

Discussion

Normally, a process's root directory is the actual system root directory, which allows the process to access any part of the filesystem. However, by using the chroot( ) system call, a process can alter its view of the filesystem by changing its root directory to another directory within the filesystem. Once the process's root directory has been changed once, it can only be made more restrictive. It is not possible to change the process's root directory to another directory outside of its current view of the filesystem.

Using chroot( ) is a simple way to increase security for processes that do not require access to the filesystem outside of a directory or hierarchy of directories containing its data files. If an attacker is somehow able to compromise the program and gain access to the filesystem, the potential for damage (whether it is reading sensitive data or destroying data) is localized to the restricted directory hierarchy imposed by altering the process's root directory.

Unfortunately, one often overlooked caveat applies to using chroot( ). The first time that chroot( ) is called, it does not necessarily alter the process's current directory, which means that until the current directory is forcibly changed, it may still be possible to access areas of the filesystem outside the new root directory structure. It is therefore imperative that the process calling chroot( ) immediately change its current directory to a directory within the new root directory structure. This is easily accomplished as follows:

#include <unistd.h>

chroot("/new/root/directory");

chdir("/");

One final point regarding the use of chroot( ) is that the system call requires the calling process to have superuser privileges.

2.13. Restricting Filesystem and Network Access on FreeBSD

Problem

Your program runs primarily (if not exclusively) on FreeBSD, and you want to impose restrictions on your program's filesystem and network capabilities that are above and beyond what chroot( ) can do. (See Recipe 2.12.)

Solution

FreeBSD implements a system call known as jail( ) , which will "imprison" a process and its descendants. It does all that chroot( ) does and more.

Discussion

Ordinarily, a jail is constructed on FreeBSD by the system administrator using the jail program, which is essentially a wrapper around the jail( ) system call. (Discounting comments and blank lines, the code is a mere 35 lines.) However, it is possible to use the jail( ) system call in your own programs.

The FreeBSD jail does everything that chroot( ) does, and then some. It restricts much of the superuser's normal abilities, and it restricts the IP address that programs running inside the jail may use.

Creating a jail is as simple as filling in a data structure with the appropriate information and calling jail( ). The same caveats that apply to chroot( ) also apply to jail( ) because jail( ) calls chroot( ) internally. In particular, only the superuser may create a jail successfully.

Presently, the jail configuration structure contains only four fields: version, path, hostname, and ip_number. The version field must be set to 0, and the path field is treated the same as chroot( )'s argument is. The hostname field sets the hostname of the jail; however, it is possible to change it from within the jail.

The ip_number field is the IP address to which processes running within the jail are restricted. Processes within the jail will only be able to bind to this address regardless of what other IP addresses are assigned to the system. In addition, all IP traffic emanating from processes within the jail will be forced to use this address as its source.

The IP address assigned to a jail must be configured on the system; typically, it should be set up as an alias rather than as the primary address for a network interface unless the network interface is dedicated to the jail. For example, a system with two network interfaces may be configured to route all traffic from processes outside the jail to one interface, and route all traffic from processes inside the jail to the other.

See Also

Recipe 2.12