Exposure of File Descriptor to Unintended Control Sphere ('File Descriptor Leak')
Description
Exposure of File Descriptor to Unintended Control Sphere occurs when a process fails to close sensitive file descriptors before spawning a child process, enabling the child to perform unauthorized I/O operations using those descriptors. When a new process is forked or executed, the child inherits open file descriptors from its parent. This creates a vulnerability when the child has fewer privileges than the parent but can still access the inherited descriptor, potentially circumventing access controls on the associated file or resource.
Risk
File descriptor leaks have severe implications. Child processes access parent's privileged files. Privilege escalation through inherited descriptors. Unauthorized read/write to sensitive files. Bypass of file permissions. Socket hijacking. Database connection theft. Log file manipulation. High likelihood when privilege separation is implemented without proper fd management.
Solution
Close all sensitive file descriptors before spawning child processes during implementation phase. Use O_CLOEXEC flag when opening files. Set FD_CLOEXEC on existing descriptors. Implement close-all-fds pattern before exec. Audit descriptor inheritance across privilege boundaries during architecture and design phase.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Unauthorized read access to application data through inherited file descriptors. |
| Integrity | Scope: Integrity Unauthorized modification of application data through inherited write descriptors. |
Example Code
Vulnerable Code
// Vulnerable: File descriptor leak to child process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/socket.h>
// VULNERABLE: Privileged fd inherited by unprivileged child
void vulnerable_privilege_separation() {
// Parent opens sensitive file as root
int shadow_fd = open("/etc/shadow", O_RDWR);
if (shadow_fd < 0) {
return;
}
pid_t pid = fork();
if (pid == 0) {
// Child process - drop privileges
setuid(getuid()); // Drop to regular user
// VULNERABLE: Child still has shadow_fd open!
// Can read/write /etc/shadow despite being unprivileged
char buffer[1024];
read(shadow_fd, buffer, sizeof(buffer)); // Works!
// Attacker can read password hashes
printf("Shadow contents: %s\n", buffer);
exit(0);
}
close(shadow_fd);
waitpid(pid, NULL, 0);
}
// VULNERABLE: Socket descriptor leak
void vulnerable_inetd_style() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind, listen ...
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
pid_t pid = fork();
if (pid == 0) {
// Child handles this client
// VULNERABLE: server_fd still open
// Child could accept other connections
// or interfere with server operation
// Drop privileges for handling untrusted client
setuid(untrusted_uid);
// Process client request
handle_request(client_fd);
// VULNERABLE: Can still use server_fd
// to accept connections as the original user
exit(0);
}
close(client_fd); // Parent closes client fd
// But child has both fds!
}
}
// VULNERABLE: Log file descriptor leak
void vulnerable_logging() {
// Open log file with elevated privileges
int log_fd = open("/var/log/secure.log", O_WRONLY | O_APPEND);
// Fork worker process
pid_t pid = fork();
if (pid == 0) {
// Worker with reduced privileges
setgid(worker_gid);
setuid(worker_uid);
// VULNERABLE: Can still write to secure log
// Could inject false log entries
const char* fake = "root logged in from 127.0.0.1\n";
write(log_fd, fake, strlen(fake));
do_work();
exit(0);
}
close(log_fd);
waitpid(pid, NULL, 0);
}
# Vulnerable: Python file descriptor inheritance
import os
import subprocess
# VULNERABLE: File descriptor inherited by subprocess
def vulnerable_subprocess():
# Open sensitive configuration
config_fd = os.open('/etc/app/secrets.conf', os.O_RDONLY)
# VULNERABLE: subprocess inherits the fd
proc = subprocess.Popen(
['untrusted_program'],
# close_fds defaults to False in older Python!
)
# untrusted_program can access the config file
# via /proc/self/fd/{config_fd}
proc.wait()
os.close(config_fd)
# VULNERABLE: Multiprocessing with shared descriptors
from multiprocessing import Process
class VulnerableWorker:
def __init__(self):
# Open privileged resource
self.secret_file = open('/root/.ssh/id_rsa', 'r')
def start_worker(self):
# VULNERABLE: Worker process inherits secret_file
p = Process(target=self.worker_main)
p.start()
return p
def worker_main(self):
# Worker can read the SSH key!
key_data = self.secret_file.read()
# ... potential exfiltration ...
Fixed Code
// Fixed: Proper file descriptor management
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <dirent.h>
// FIXED: Close sensitive fd before fork
void safe_privilege_separation() {
int shadow_fd = open("/etc/shadow", O_RDWR);
if (shadow_fd < 0) {
return;
}
// Read what we need while privileged
char shadow_data[4096];
ssize_t len = read(shadow_fd, shadow_data, sizeof(shadow_data) - 1);
// FIXED: Close before fork
close(shadow_fd);
shadow_fd = -1;
pid_t pid = fork();
if (pid == 0) {
// Child - no access to shadow_fd
setuid(getuid());
// Process data that was already read
// Cannot access /etc/shadow directly
exit(0);
}
// Clear sensitive data
memset(shadow_data, 0, sizeof(shadow_data));
waitpid(pid, NULL, 0);
}
// FIXED: Use O_CLOEXEC flag
void safe_with_cloexec() {
// FIXED: File will be closed on exec
int secret_fd = open("/etc/shadow", O_RDONLY | O_CLOEXEC);
pid_t pid = fork();
if (pid == 0) {
// If we call exec, secret_fd will be closed automatically
// For fork-only, we still need to close manually
// FIXED: Close in child if not using exec
close(secret_fd);
// Now safe to drop privileges
setuid(getuid());
// ... do work ...
exit(0);
}
close(secret_fd);
waitpid(pid, NULL, 0);
}
// FIXED: Close all file descriptors
void close_all_fds(void) {
// Method 1: Use /proc/self/fd
DIR* dir = opendir("/proc/self/fd");
if (dir) {
int dir_fd = dirfd(dir);
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
int fd = atoi(entry->d_name);
if (fd > STDERR_FILENO && fd != dir_fd) {
close(fd);
}
}
closedir(dir);
return;
}
// Method 2: Iterate through possible fds
struct rlimit rl;
getrlimit(RLIMIT_NOFILE, &rl);
for (int fd = STDERR_FILENO + 1; fd < (int)rl.rlim_cur; fd++) {
close(fd);
}
}
// FIXED: Safe server with fd management
void safe_inetd_style() {
// FIXED: Server socket with close-on-exec
int server_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
// ... bind, listen ...
while (1) {
// FIXED: Accept with close-on-exec
int client_fd = accept4(server_fd, NULL, NULL, SOCK_CLOEXEC);
pid_t pid = fork();
if (pid == 0) {
// FIXED: Close server fd in child
close(server_fd);
// Clear close-on-exec for client_fd if needed
fcntl(client_fd, F_SETFD, 0);
// Now safe to drop privileges
setuid(untrusted_uid);
handle_request(client_fd);
close(client_fd);
exit(0);
}
close(client_fd);
}
}
// FIXED: Set close-on-exec on existing fd
void set_cloexec(int fd) {
int flags = fcntl(fd, F_GETFD);
if (flags >= 0) {
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
}
# Fixed: Python with proper fd isolation
import os
import subprocess
import fcntl
# FIXED: Close fds before subprocess
def safe_subprocess():
config_fd = os.open('/etc/app/secrets.conf', os.O_RDONLY)
# Read what we need
config_data = os.read(config_fd, 4096)
# FIXED: Close before subprocess
os.close(config_fd)
# FIXED: close_fds=True (default in Python 3.2+)
proc = subprocess.Popen(
['untrusted_program'],
close_fds=True # Explicitly set for clarity
)
proc.wait()
# FIXED: Use O_CLOEXEC equivalent
def safe_open_cloexec(path, flags):
fd = os.open(path, flags)
# FIXED: Set close-on-exec flag
fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
return fd
# FIXED: Multiprocessing with proper isolation
from multiprocessing import Process
import multiprocessing
class SafeWorker:
def __init__(self):
# Don't open sensitive files in parent
self.secret_path = '/root/.ssh/id_rsa'
def start_worker(self, task_data):
# FIXED: Pass only necessary data, not file handles
p = Process(target=self.worker_main, args=(task_data,))
p.start()
return p
def worker_main(self, task_data):
# Worker opens its own files with appropriate permissions
# No inherited sensitive descriptors
pass
# FIXED: Context manager for automatic cleanup
class SecureFD:
def __init__(self, path, flags):
self.fd = os.open(path, flags | os.O_CLOEXEC)
def __enter__(self):
return self.fd
def __exit__(self, *args):
os.close(self.fd)
# Usage
with SecureFD('/etc/shadow', os.O_RDONLY) as fd:
data = os.read(fd, 4096)
# fd automatically closed, safe to fork/exec
CVE Examples
- CVE-2003-0740: Sendmail file descriptor leak allowed privilege escalation.
- CVE-2003-0937: OpenSSH file descriptor leak in privilege separation.
- CVE-2006-5397: Screen file descriptor leak permitted unauthorized access.
Related CWEs
- CWE-402: Transmission of Private Resources into a New Sphere (parent)
- CWE-399: Resource Management Errors (category)
References
- MITRE Corporation. "CWE-403: Exposure of File Descriptor to Unintended Control Sphere." https://cwe.mitre.org/data/definitions/403.html
- CERT C. "FIO42-C. Close files when they are no longer needed"
- OpenBSD. "Privilege Separation Guidelines"