CWE-243: Creation of chroot Jail Without Changing Working Directory
Overview
Creating a chroot jail without changing the working directory allows processes to escape the jail by using relative paths (../..) to traverse back to the original working directory, completely bypassing the chroot sandbox and accessing the full filesystem.
Risk
High: Failing to chdir() after chroot() allows chroot escape via relative path traversal, enabling access to files outside the jail, privilege escalation, reading sensitive system files (/etc/shadow, /etc/passwd), and complete sandbox bypass. The chroot is effectively useless.
Remediation Steps
Core principle: Do not rely on implicit assumptions about shared state; validate state transitions and invariants explicitly.
Locate Incorrect chroot Usage
When reviewing security scan results:
- Find chroot() calls: Identify where chroot jails are created
- Check for chdir() after chroot: Look for missing directory change
- Verify privilege dropping: Check if code drops root after chroot
- Review working directory: Ensure CWD is inside jail
- Check file operations: Look for relative path usage that could escape
Vulnerable pattern:
// chroot() without chdir() - VULNERABLE!
chroot("/var/jail");
// Current working directory still outside jail!
// Can escape with: open("../../../etc/shadow")
Always chdir After chroot (Primary Defense)
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pwd.h>
// VULNERABLE - chroot without chdir
void setup_jail_bad() {
// Create jail
if (chroot("/var/jail") != 0) {
perror("chroot failed");
exit(1);
}
// BUG: Current working directory still outside jail!
// Attacker can use relative paths:
// open("../../../etc/shadow") escapes the jail
// Continue running as root - ALSO WRONG!
}
// SECURE - chdir immediately after chroot
void setup_jail_safe() {
// 1. chroot to jail directory
if (chroot("/var/jail") != 0) {
perror("chroot failed");
exit(1);
}
// 2. CRITICAL: Change to root of jail
if (chdir("/") != 0) {
perror("chdir failed");
exit(1);
}
// 3. Drop privileges (root can escape chroot!)
struct passwd *pw = getpwnam("nobody");
if (pw == NULL) {
fprintf(stderr, "User 'nobody' not found\n");
exit(1);
}
// Drop supplementary groups
if (setgroups(0, NULL) != 0) {
perror("setgroups failed");
exit(1);
}
// Set GID
if (setgid(pw->pw_gid) != 0) {
perror("setgid failed");
exit(1);
}
// Set UID (do this last!)
if (setuid(pw->pw_uid) != 0) {
perror("setuid failed");
exit(1);
}
// Verify we're not root
if (getuid() == 0 || geteuid() == 0) {
fprintf(stderr, "Failed to drop root privileges!\n");
exit(1);
}
printf("Jail setup complete. Running as UID: %d\n", getuid());
}
Why this works: After chroot("/var/jail"), the root directory / is remapped to /var/jail. But the current working directory remains unchanged outside the jail. Doing chdir("/") changes to the jail's root, preventing relative path traversal escape.
Drop Privileges After Jail Setup
#include <unistd.h>
#include <sys/capability.h> // libcap
// VULNERABLE - running as root after chroot
void run_service_bad() {
chroot("/var/jail");
chdir("/");
// STILL ROOT - can escape chroot!
// Root can: create device nodes, use chroot again, etc.
run_network_service(); // Runs as root!
}
// SECURE - drop all privileges
void run_service_safe() {
// Setup jail
if (chroot("/var/jail") != 0) {
perror("chroot");
exit(1);
}
if (chdir("/") != 0) {
perror("chdir");
exit(1);
}
// Drop all capabilities (Linux)
cap_t caps = cap_init();
if (cap_set_proc(caps) != 0) {
perror("cap_set_proc");
exit(1);
}
cap_free(caps);
// Change to non-privileged user
if (setgid(65534) != 0) { // nobody group
perror("setgid");
exit(1);
}
if (setuid(65534) != 0) { // nobody user
perror("setuid");
exit(1);
}
// Verify not root
if (getuid() == 0) {
fprintf(stderr, "Still running as root!\n");
exit(1);
}
// Now safe to run service
run_network_service();
}
Use Modern Sandboxing Alternatives
# VULNERABLE - chroot alone is insufficient
# Don't use for security isolation!
# BETTER - Use containers (Docker)
docker run --rm -it \
--read-only \
--tmpfs /tmp \
--user 1000:1000 \
--cap-drop ALL \
--security-opt no-new-privileges \
--network none \
myapp
# BETTER - Use systemd with namespaces
# /etc/systemd/system/myapp.service
[Service]
ExecStart=/usr/bin/myapp
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadOnlyPaths=/
NoNewPrivileges=yes
CapabilityBoundingSet=
# BETTER - Use firejail
firejail --private --net=none --seccomp myapp
# Python - use containers instead of chroot
import docker
client = docker.from_env()
# Run in isolated container
container = client.containers.run(
'python:3.9-slim',
'python /app/script.py',
volumes={'/path/to/app': {'bind': '/app', 'mode': 'ro'}},
user='1000:1000',
read_only=True,
tmpfs={'/tmp': 'size=100M'},
cap_drop=['ALL'],
security_opt=['no-new-privileges'],
network_mode='none',
remove=True
)
Validate Jail Environment
#!/bin/bash
# Prepare secure chroot jail
JAIL_DIR="/var/jail"
# Create jail structure
mkdir -p "$JAIL_DIR"/{bin,lib,lib64,etc,tmp,dev}
# Copy only necessary binaries
cp /bin/sh "$JAIL_DIR/bin/"
# Copy required libraries
for lib in $(ldd /bin/sh | grep -o '/lib[^ ]*'); do
cp --parents "$lib" "$JAIL_DIR/"
done
# Create minimal /etc/passwd (no passwords!)
cat > "$JAIL_DIR/etc/passwd" << EOF
nobody:x:65534:65534:Nobody:/:/bin/false
EOF
# Set strict permissions
chmod 755 "$JAIL_DIR"
chmod 1777 "$JAIL_DIR/tmp" # Sticky bit
# NO setuid binaries!
find "$JAIL_DIR" -type f -perm /4000 -delete
# NO device files!
find "$JAIL_DIR/dev" -type b -delete
find "$JAIL_DIR/dev" -type c -delete
# Create minimal safe devices if needed
mknod -m 666 "$JAIL_DIR/dev/null" c 1 3
mknod -m 666 "$JAIL_DIR/dev/zero" c 1 5
mknod -m 444 "$JAIL_DIR/dev/random" c 1 8
mknod -m 444 "$JAIL_DIR/dev/urandom" c 1 9
echo "Jail prepared at $JAIL_DIR"
Test chroot Jail Security
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void test_jail_escape_prevented() {
printf("Testing chroot jail security...\n");
// Setup jail correctly
if (chroot("/var/jail") != 0) {
perror("chroot");
return;
}
if (chdir("/") != 0) {
perror("chdir");
return;
}
// Drop privileges
if (setuid(65534) != 0) {
perror("setuid");
return;
}
// Test 1: Cannot access files outside jail
int fd = open("../../../etc/shadow", O_RDONLY);
if (fd == -1) {
printf("✓ Cannot escape jail with relative paths\n");
} else {
printf("✗ JAIL ESCAPE POSSIBLE!\n");
close(fd);
}
// Test 2: Cannot see real root
fd = open("/etc/shadow", O_RDONLY);
if (fd == -1) {
printf("✓ Cannot access /etc/shadow from jail\n");
} else {
printf("✗ Can access real /etc/shadow!\n");
close(fd);
}
// Test 3: Not running as root
if (getuid() != 0 && geteuid() != 0) {
printf("✓ Not running as root (UID: %d)\n", getuid());
} else {
printf("✗ Still running as root!\n");
}
// Test 4: Cannot chroot again (requires root)
if (chroot("/tmp") == -1) {
printf("✓ Cannot chroot again (not root)\n");
} else {
printf("✗ Can chroot again - privilege issue!\n");
}
}
Common Vulnerable Patterns
- chroot() without chdir()
- chroot() while still running as root
- Assuming chroot provides complete isolation
- Not validating chroot/chdir return values
- Leaving privileged files in jail
Security Checklist
- chdir("/") called immediately after chroot()
- Both chroot() and chdir() return values checked
- Privileges dropped after jail setup (setuid/setgid to non-root)
- Verified not running as root after dropping privileges
- No setuid binaries in jail
- No device files in jail (except safe ones: null, zero, random)
- Jail directory has minimal files
- Modern alternatives considered (containers, namespaces)
- Tests verify escape attempts fail