...
Performing file operations in a secure directory eliminates the possibility that an attacker might tamper with the files or file system to exploit a file system vulnerability in a program. These vulnerabilities often exist because there is a loose binding between the file name and the actual file. (See recommendation FIO01-C. Be careful using functions that use file names for identification.) In some cases, file operations can be performed securely anywhere. In other cases, the only way to ensure secure file operations is to perform the operation within a secure directory.
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
char *file_name;
FILE *fp;
/* initialize file_name */
fp = fopen(file_name, "w");
if (fp == NULL) {
/* Handle error */
}
/*... Process file ...*/
if (fclose(fp) != 0) {
/* Handle error */
}
if (remove(file_name) != 0) {
/* Handle error */
}
|
An attacker can replace the file object identified by file_name with a link to an arbitrary file before the call to fopen(). It is also possible that the file object identified by file_name in the call to remove() is not the same file object identified by file_name in the call to fopen(). If the file is not in a secure directory, for example, /tmp/app/tmpdir/passwd, then an attacker can manipulate the location of the file as follows:
| Code Block |
|---|
% cd /tmp/app/
% rm -rf tmpdir
% ln -s /etc tmpdir
|
...
fullpath need not be canonicalized (See FIO02-C. Canonicalize path names originating from untrusted sources). If the path contains a symbolic link, this routine will recursively invoke invokes itself on the linked-to directory and ensure ensures it is also secure. A symbolically - linked directory may be secure if both its source and linked-to directory are secure.
Note that this function is effective only effective on filesystems file systems that are fully compatible with UNIX permissions, and it may not behave normally for filesystems file systems with other permission mechanisms, such as AFS (Andrew File System).
| Code Block | ||||
|---|---|---|---|---|
| ||||
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <libgen.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
enum { MAX_SYMLINKS = 5 };
/* Returns nonzero if directory is secure, zero otherwise */
int secure_dir(const char *fullpath) {
static unsigned int num_symlinks = 0;
char *path_copy = NULL;
char **dirs = NULL;
int num_of_dirs = 1;
int secure = 1;
int i, r;
struct stat buf;
uid_t my_uid = geteuid();
size_t linksize;
char* link;
if (!fullpath || fullpath[0] != '/') {
/* Handle error */
}
if (num_symlinks > MAX_SYMLINKS) { // Could be a symlink loop
/* Handle error */
}
if (!(path_copy = strdup(fullpath))) {
/* Handle error */
}
// Figure out how far it is to the root
for (; ((strcmp(path_copy, "/") != 0) &&
(strcmp(path_copy, "//") != 0));
path_copy = dirname(path_copy)) {
num_of_dirs++;
} // now num_of_dirs indicates # of dirs we must check
free(path_copy);
path_copy = NULL;
if (!(dirs = (char **)malloc(num_of_dirs * sizeof(char *)))) {
/* Handle error */
}
if (!(dirs[num_of_dirs - 1] = strdup(fullpath))) {
/* Handle error */
}
if (!(path_copy = strdup(fullpath))) {
/* Handle error */
}
// Now fill the dirs array
for (i = num_of_dirs - 2; i >= 0; i--) {
path_copy = dirname(path_copy);
if (!(dirs[i] = strdup(path_copy))) {
/* Handle error */
}
}
free(path_copy);
path_copy = NULL;
// Traverse from the root to the fullpath,
// checking permissions along the way
for (i = 0; i < num_of_dirs; i++) {
if (lstat(dirs[i], &buf) != 0) {
/* Handle error */
}
if (S_ISLNK(buf.st_mode)) { // symlink, test linked-to file
linksize = buf.st_size + 1;
if (!(link = (char *)malloc(linksize))) {
/* Handle error */
}
r = readlink(dirs[i], link, linksize);
if (r == -1) {
/* Handle error */
} else if (r >= linksize) {
/* Handle truncation error */
}
link[r] = '\0';
num_symlinks++;
r = secure_dir(link);
num_symlinks--;
if (!r) {
secure = 0;
free(link);
link = NULL;
break;
}
free(link);
link = NULL;
continue;
}
if (!S_ISDIR(buf.st_mode)) { // not a directory
secure = 0;
break;
}
if ((buf.st_uid != my_uid) && (buf.st_uid != 0)) {
// Directory is owned by someone besides user or root
secure = 0;
break;
}
if (buf.st_mode & (S_IWGRP | S_IWOTH)) { // dir is writable by others
secure = 0;
break;
}
}
for (i = 0; i < num_of_dirs; i++) {
free(dirs[i]);
dirs[i] = NULL;
}
free(dirs);
dirs = NULL;
return secure;
}
|
This compliant solution uses this the secure_dir() function to ensure that an attacker may not tamper with the file to be opened and subsequently removed. Note that once the path name of a directory has been checked using secure_dir(), all further file operations on that directory must be performed using the same path.
| Code Block | ||||
|---|---|---|---|---|
| ||||
char *dir_name;
const char *file_name = "passwd"; /* file name within the secure directory */
FILE *fp;
/* initialize dir_name */
if (!secure_dir(dir_name)) {
/* Handle error */
}
if (chdir(dir_name) == -1) {
/* Handle error */
}
fp = fopen(file_name, "w");
if (fp == NULL) {
/* Handle error */
}
/*... Process file ...*/
if (fclose(fp) != 0) {
/* Handle error */
}
if (remove(file_name) != 0) {
/* Handle error */
}
|
...
CERT C++ Secure Coding Standard: FIO15-CPP. Ensure that file operations are performed in a secure directory
MITRE CWE: CWE-552, "Files or Directories Accessible to External Partiesdirectories accessible to external parties"
MITRE CWE: CWE-379, "Creation of Temporary File temporary file in Directory directory with Insecure Permissionsinsecure permissions"
Bibliography
[Open Group 2004] dirname(), realpath()
[Viega 2003] Section 2.4, "Determining Whether a Directory Is Securewhether a directory is secure"
...