flawopen.com/Path Traversal/C/C++

Path Traversal in C/C++

High Severity CWE-22
ELI5

In C, combining strings to open a file is like gluing together pieces of paper. If an attacker puts '../../' into the glue, the OS happily opens the root password file unless you call realpath() to check where it actually points.

Key terms on this page
realpath(3)
POSIX library function that resolves all symbolic links, extra slashes, and relative references (./, ../) returning a null-terminated absolute path.
PATH_MAX
Operating system constant defining the maximum number of bytes in a pathname string.

What's happening

In C and C++, constructing paths with snprintf(path, sizeof(path), "%s/%s", base_dir, user_filename) and passing them to open() or fopen() allows traversal tokens to escape the intended directory tree.

Real-world impact

High-severity vulnerabilities in embedded firmware, web servers (e.g. Apache CVE-2021-41773), and network daemons frequently originate from path traversal in C code.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
// VULNERABLE: Direct snprintf and open without realpath check
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int serve_user_file(const char *user_filename) {
    char full_path[1024];
    const char *base_dir = "/var/app/public/files";

    // Attacker input: "../../../../etc/shadow"
    snprintf(full_path, sizeof(full_path), "%s/%s", base_dir, user_filename);

    // Directly opens system shadow file!
    int fd = open(full_path, O_RDONLY);
    return fd;
}
FIXED
// HARDENED: Canonicalize with realpath and verify directory prefix bounds
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>

int serve_user_file(const char *user_filename) {
    const char *base_dir = "/var/app/public/files";
    char base_canonical[PATH_MAX];
    char raw_path[PATH_MAX];
    char target_canonical[PATH_MAX];

    // 1. Resolve canonical base directory
    if (realpath(base_dir, base_canonical) == NULL) {
        return -1;
    }

    // 2. Safely construct raw path
    if (snprintf(raw_path, sizeof(raw_path), "%s/%s", base_canonical, user_filename) >= (int)sizeof(raw_path)) {
        return -1; // Path truncated
    }

    // 3. Resolve canonical target path
    if (realpath(raw_path, target_canonical) == NULL) {
        return -1; // File does not exist or access error
    }

    // 4. Verify canonical target begins with base_canonical + '/'
    size_t base_len = strlen(base_canonical);
    if (strncmp(target_canonical, base_canonical, base_len) != 0 || 
        target_canonical[base_len] != '/') {
        return -1; // Traversal detected!
    }

    return open(target_canonical, O_RDONLY | O_CLOEXEC);
}

Why the fix works

realpath() resolves all traversal tokens and symbolic links. Checking strncmp(target_canonical, base_canonical, base_len) == 0 and verifying that the character immediately following is a directory slash (/) prevents both traversal and sibling directory access.

Gotchas

Buffer truncation in snprintf

Always check the return value of snprintf; if it equals or exceeds sizeof(buffer), truncation occurred, which can truncate file extensions or boundaries.

Common misconceptions

"strstr(filename, '..') is safe enough"

String checking can be bypassed with URL encoding, null byte injection, or unexpected filesystem encoding sequences.

How to check if you're affected

flawfinder . scan-build make

Prevention checklist

FAQ

What if the file doesn't exist yet?

If creating a new file, call realpath() on the parent directory to verify boundary containment before opening with O_CREAT.

References