Why File Operations are a Security Minefield
File operations seem simple โ open a file, write some data, close it. But for privileged programs, every file operation is a potential attack vector. The permissions on a newly created file, where it’s created, how it’s named, and the order of permission-setting operations can all be exploited by a malicious user if not handled carefully.
The umask is a process-wide mask that removes permission bits from newly created files. It is inherited by child processes and survives exec(). A privileged program should always set a restrictive umask to ensure no file is accidentally created as world-writable or world-readable.
If a root-owned file is world-writable, any user can modify it โ completely bypassing the privilege model. A malicious user could wait for the privileged program to create such a file and immediately write malicious content to it.
| umask value | Permissions removed | Resulting file mode (if open with 0666) | Security level |
|---|---|---|---|
| 0000 | Nothing removed | rw-rw-rw- (world-writable!) | Dangerous |
| 0022 | Group and other write | rw-r–r– | Acceptable |
| 0077 | All group and other access | rw——- (owner only) | Best for sensitive files |
| 0177 | All except owner read+write | rw——- (effective) | Secure |
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
/* Set restrictive umask at program startup.
* This means any file created will be at most rw------- (0600)
* even if open() is called with mode 0666. */
umask(0177);
/* Create a file โ permissions will be 0600 (rw-------) */
int fd = open("/tmp/secure_file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
perror("open");
return 1;
}
/* Actual permissions = 0666 & ~0177 = 0600 = rw------- */
printf("File created. Run: ls -la /tmp/secure_file.txt\n");
close(fd);
return 0;
}
When a privileged program creates a file and the security depends on being the sole creator (not opening an attacker-planted file), use O_EXCL with O_CREAT. The combination O_CREAT | O_EXCL is atomic โ if the file already exists, open() fails with EEXIST. There is no TOCTTOU window.
Without O_EXCL, an attacker could pre-create the file (as a symlink to /etc/passwd), and your program would open it instead of creating a new file.
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
/*
* Creates a file exclusively โ fails if it already exists.
* This is an atomic check-and-create, preventing TOCTTOU.
*/
int create_exclusively(const char *path)
{
int fd;
/* O_CREAT | O_EXCL: atomically create and open.
* Fails with EEXIST if file already exists. */
fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd == -1) {
if (errno == EEXIST) {
fprintf(stderr, "File already exists: %s (possible attack!)\n", path);
} else {
perror("open");
}
return -1;
}
printf("Created new file: %s (fd=%d)\n", path, fd);
return fd;
}
int main(void)
{
int fd = create_exclusively("/tmp/my_config_1234.tmp");
if (fd != -1) {
/* We are the guaranteed creator of this file */
write(fd, "secure config data\n", 19);
close(fd);
}
return 0;
}
/tmp is a world-writable directory โ any user can create files there. When a privileged program creates a predictable filename in /tmp, an attacker can pre-create that file (as a symlink or regular file) before the privileged program does. This is called a symlink attack.
If you absolutely must use a temporary file:
Creates a file with a random, unpredictable name. Returns an open FD. Atomically creates and opens.
Create a directory owned by root (mode 0700) and create temp files inside it. Attacker can’t pre-create files there.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
char template[] = "/tmp/myprog_XXXXXX";
/* mkstemp() replaces XXXXXX with a random string.
* It creates the file atomically and returns an open FD.
* File permissions are 0600 โ only owner can read/write. */
int fd = mkstemp(template);
if (fd == -1) {
perror("mkstemp");
return 1;
}
printf("Created temp file: %s (fd=%d)\n", template, fd);
/* Optional: unlink the file immediately so it disappears
* when closed โ the data is accessible only through the FD. */
unlink(template);
printf("File unlinked (will disappear on close, data still accessible via fd)\n");
/* Write to it via FD */
const char *data = "temporary sensitive data\n";
write(fd, data, strlen(data));
close(fd);
printf("Temp file closed and gone.\n");
return 0;
}
If a set-UID-root program creates a file that initially needs to be owned by root but will later be handed to another user, the ownership transfer must be done carefully.
The correct sequence: create the file as non-world-writable first, then change ownership with fchown() (on the FD), then set final permissions with fchmod() (on the FD). Using chown(path, ...) instead of fchown(fd, ...) creates a TOCTTOU window on the path.
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
/*
* Creates a file as root, then transfers ownership to another user.
* Uses FD-based calls (fchown, fchmod) to avoid TOCTTOU.
*/
int create_and_transfer(const char *path, uid_t target_uid, gid_t target_gid)
{
int fd;
/* Step 1: Create file โ initially NOT world-writable.
* Use 0600 (owner read-write only) while we still own it. */
fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd == -1) {
perror("open");
return -1;
}
/* Step 2: Write initial content */
write(fd, "config data for user\n", 21);
/* Step 3: Transfer ownership using FD (not path) โ no TOCTTOU */
if (fchown(fd, target_uid, target_gid) == -1) {
perror("fchown");
close(fd);
unlink(path);
return -1;
}
/* Step 4: Set final permissions using FD (not path) */
if (fchmod(fd, 0644) == -1) {
perror("fchmod");
close(fd);
unlink(path);
return -1;
}
printf("File %s created and transferred to UID=%d GID=%d\n",
path, (int)target_uid, (int)target_gid);
close(fd);
return 0;
}
int main(void)
{
/* This would be run as root in a real scenario */
create_and_transfer("/tmp/user_config.conf", 1000, 1000);
return 0;
}
