Skip to content

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

Additional Resources