Back to Blog
critical SEVERITY9 min read

Path Traversal in ZMODEM Receiver: How a Missing basename() Call Could Overwrite Your SSH Keys

A critical path traversal vulnerability in a ZMODEM file receiver allowed a malicious sender to supply crafted filenames containing directory traversal sequences (like `../../.ssh/authorized_keys`), causing the receiver to write file contents to arbitrary locations on the filesystem. The fix strips path separators and validates filenames before use, ensuring received files can only be written to the intended download directory. This class of vulnerability is a stark reminder that any input origi

O
By orbisai0security
May 28, 2026

Path Traversal in ZMODEM Receiver: How a Missing basename() Call Could Overwrite Your SSH Keys

Introduction

File transfer protocols have been around for decades, and ZMODEM — a protocol born in the 1980s — is still alive and well in terminal emulators, embedded systems, and serial communication tools. But age doesn't mean immunity from security vulnerabilities. A recently patched critical vulnerability in a ZMODEM receiver implementation demonstrates how a single missing validation step can turn a routine file download into a full filesystem compromise.

The vulnerability? A classic path traversal attack (also known as a directory traversal attack), enabled by blindly trusting the filename supplied by the remote sender. No stripping of path separators. No call to basename(). No validation that the resolved output path stays within the intended download directory.

If you write code that accepts filenames from remote sources — whether via file transfer protocols, APIs, or file upload endpoints — this post is for you.


What Is a Path Traversal Vulnerability?

A path traversal vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory) occurs when an application uses externally supplied input to construct a file path without adequately neutralizing special elements like .. (parent directory), / (Unix path separator), or \ (Windows path separator).

The result: an attacker can "escape" the intended directory and read from or write to arbitrary locations on the filesystem.

In the context of a file receiver, this is especially dangerous. The attacker controls the filename that gets written to disk. If the receiver doesn't sanitize that filename, the attacker controls the destination of the write — and can potentially overwrite any file the receiving process has permission to modify.


The Vulnerability Explained

Where It Lived

The vulnerability existed in lib/qzmodem/qrecvzmodem.cpp, specifically in the rz_process_header() function — the code responsible for processing the file header sent by the remote ZMODEM sender. This header includes, among other things, the filename of the file being transferred.

Here's the vulnerable code path (simplified):

// VULNERABLE CODE (before fix)
name_static = (char *)malloc(strlen(name) + 1);
if (!name_static) {
    qFatal("out of memory");
}
strcpy(name_static, name);  // name comes directly from the remote sender!
zi->fname = name_static;

// ... later ...
pathname = (char *)malloc((PATH_MAX) * 2);
if (!pathname) {
    qFatal("out of memory");
}
strcpy(pathname, name_static);  // written directly as the output path

Notice what's not happening here:
- No stripping of / or \ characters
- No removal of .. traversal sequences
- No call to basename() to extract just the filename component
- No realpath() validation to confirm the resolved path stays within a permitted directory

How Could It Be Exploited?

The attack is straightforward. A malicious ZMODEM sender crafts a file transfer where the filename in the ZMODEM header is not a simple name like document.pdf, but instead a traversal path:

../../.ssh/authorized_keys

or

/etc/cron.d/backdoor

or on Windows:

..\..\AppData\Roaming\malware.exe

The receiver dutifully takes this filename, constructs a path from it, and writes the received file contents to that location. If the receiving process runs with sufficient privileges — or if the target file is writable by the current user — the attacker has just overwritten an arbitrary file with arbitrary content.

Real-World Attack Scenario

Imagine a terminal emulator using this ZMODEM implementation to receive files from a remote server over SSH. A compromised server (or a man-in-the-middle attacker on an unencrypted serial connection) initiates a ZMODEM transfer with the filename ../../.ssh/authorized_keys. The receiver writes the attacker's public key to the user's SSH authorized keys file. The attacker now has persistent SSH access to the victim's machine — without ever needing the user's password.

Alternatively, on a system where the terminal emulator runs as root (common in embedded or industrial systems), the attacker could write to /etc/cron.d/backdoor and establish a cron job for persistent code execution.

CVSS Characteristics:
- Attack Vector: Network (via the file transfer session)
- Attack Complexity: Low (no special conditions required)
- Privileges Required: None (beyond the ability to initiate a ZMODEM transfer)
- Impact: High (arbitrary file write → potential full system compromise)

This is why the severity is rated Critical.


The Fix

The fix addresses the vulnerability at the point where the remote-supplied filename is first processed, before it is ever used to construct a filesystem path.

Before and After

Before (vulnerable):

name_static = (char *)malloc(strlen(name) + 1);
if (!name_static) {
    qFatal("out of memory");
}
strcpy(name_static, name);  // raw remote input, no sanitization
zi->fname = name_static;

After (fixed):

/* strip path separators to prevent directory traversal */
p = strrchr(name, '/');
if (!p) p = strrchr(name, '\\');
if (p) name = p + 1;
if (*name == '\0' || strcmp(name, "..") == 0 || strcmp(name, ".") == 0)
    name = (char *)"unnamed";

name_static = (char *)malloc(strlen(name) + 1);
if (!name_static) {
    qFatal("out of memory");
}
memcpy(name_static, name, strlen(name) + 1);  // sanitized name
zi->fname = name_static;

Breaking Down the Fix

Let's walk through each defensive step:

1. Strip Path Separators (Both Unix and Windows)

p = strrchr(name, '/');
if (!p) p = strrchr(name, '\\');
if (p) name = p + 1;

strrchr() finds the last occurrence of the path separator character. By taking everything after the last separator (p + 1), we effectively call the equivalent of basename() — extracting only the final component of the path.

For example:
- ../../.ssh/authorized_keysauthorized_keys
- /etc/cron.d/backdoorbackdoor
- ..\..\AppData\malware.exemalware.exe

Checking both / and \ is important for cross-platform robustness, especially since ZMODEM senders may run on different operating systems.

2. Reject Degenerate Filenames

if (*name == '\0' || strcmp(name, "..") == 0 || strcmp(name, ".") == 0)
    name = (char *)"unnamed";

After stripping separators, we must handle edge cases:
- An empty string (e.g., if the original name was just / or \)
- The literal string .. (parent directory reference)
- The literal string . (current directory reference)

These are all replaced with the safe fallback "unnamed".

3. memcpy Instead of strcpy

memcpy(name_static, name, strlen(name) + 1);

The fix also replaces strcpy() with memcpy(). While both are functionally equivalent for null-terminated strings when the destination is correctly sized, memcpy() with an explicit length is considered a better practice — it makes the intent explicit and is less prone to misuse in future refactoring. (Note: strncpy() or strlcpy() would also be reasonable choices here.)

4. calloc Instead of malloc for Pathname Buffer

pathname = (char *)calloc(PATH_MAX, 2);

The malloc call for the pathname buffer was replaced with calloc, which zero-initializes the allocated memory. This eliminates the possibility of uninitialized memory being used as part of a path if subsequent string operations don't fill the entire buffer.

5. snprintf Instead of strcpy for Pathname Construction

snprintf(pathname, PATH_MAX * 2, "%s", name_static);

The final strcpy into pathname is replaced with snprintf, which respects the buffer size limit and prevents potential buffer overflows if the sanitized name somehow exceeds the expected length.


Prevention & Best Practices

This vulnerability is a textbook example of trusting remote input. Here's how to avoid it in your own code:

1. Never Trust Filenames from Remote Sources

Any filename that originates outside your process — from a network peer, an API, a file upload, a database — must be treated as hostile input until proven otherwise.

2. Use basename() or Equivalent

In C/C++, basename() (POSIX) extracts the final component of a path. Always apply it (or an equivalent) to remote-supplied filenames before using them in path construction:

#include <libgen.h>

char *safe_name = basename(remote_filename);

⚠️ Note: basename() modifies its argument on some platforms. Use a copy, and be aware of platform differences between POSIX and GNU behavior.

3. Validate the Resolved Path with realpath()

After constructing the full output path, use realpath() to resolve all symlinks and .. components, then verify the result starts with your intended base directory:

char resolved[PATH_MAX];
if (realpath(output_path, resolved) == NULL) {
    // path doesn't exist yet — handle carefully
}
if (strncmp(resolved, allowed_base_dir, strlen(allowed_base_dir)) != 0) {
    // path escapes the allowed directory — reject it
    error("Path traversal detected!");
}

4. Allowlist Valid Filename Characters

Consider restricting filenames to a known-safe character set (alphanumerics, hyphens, underscores, dots) using a regex or character-by-character validation:

bool is_safe_filename(const char *name) {
    for (const char *p = name; *p; p++) {
        if (!isalnum(*p) && *p != '-' && *p != '_' && *p != '.') {
            return false;
        }
    }
    return strlen(name) > 0 && strcmp(name, "..") != 0 && strcmp(name, ".") != 0;
}

5. Use Safer String Functions

Replace strcpy() with bounded alternatives:
- strlcpy() (BSD/macOS, or via a compatibility library)
- snprintf(dst, size, "%s", src)
- strncpy() (with care — it doesn't guarantee null-termination)

6. Apply the Principle of Least Privilege

File receivers should run with the minimum privileges necessary. If the receiver only needs to write to a specific download directory, it should not run as root or with write access to sensitive system files.

7. Use Static Analysis Tools

Tools like these can catch path traversal vulnerabilities during development:
- Coverity — detects tainted data flowing into file operations
- CodeQL — GitHub's semantic code analysis engine (has path traversal queries)
- Semgrep — customizable pattern-based scanner
- Flawfinder — lightweight C/C++ scanner that flags dangerous function calls like strcpy

Relevant Security Standards

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • CWE-73: External Control of File Name or Path
  • OWASP: Path Traversal
  • OWASP Testing Guide: OTG-AUTHZ-001 (Directory Traversal)
  • CERT C: FIO02-C (Canonicalize path names originating from tainted sources)

Conclusion

A single missing call to basename() — a function that's been in POSIX for decades — was enough to create a critical, filesystem-level vulnerability in a ZMODEM receiver. The attack requires no special privileges, no complex exploit chain, and no memory corruption: just a crafted filename and a receiver that trusts it.

The fix is elegant in its simplicity: strip path separators, reject degenerate names, use bounded string functions, and zero-initialize buffers. These are not exotic techniques — they're basic defensive programming practices that every developer working with file paths and remote input should have in their toolkit.

Key takeaways:

  • 🚨 Remote-supplied filenames are untrusted input. Always sanitize them before use in filesystem operations.
  • 🛡️ basename() and realpath() are your friends. Use them together for robust path validation.
  • 📏 Prefer bounded string functions (snprintf, strlcpy, memcpy with explicit sizes) over unbounded ones (strcpy, strcat).
  • 🔍 Static analysis tools can catch these vulnerabilities automatically — integrate them into your CI/CD pipeline.
  • 🔒 Least privilege matters. Even if a traversal occurs, limiting the process's filesystem permissions reduces the blast radius.

File transfer code is often old, battle-tested, and rarely touched — which is exactly why it deserves a fresh security review. The next time you're working with any code that writes files based on externally supplied names, ask yourself: what happens if that name is ../../etc/passwd?


This vulnerability was identified and fixed by automated security scanning. Automated tools can catch entire classes of vulnerabilities like path traversal consistently and at scale — making them an essential complement to manual code review.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #67

Related Articles

critical

Heap Buffer Overflow in Path Normalization: How Two Unsafe memcpy Calls Almost Became a Critical Exploit

A critical heap buffer overflow vulnerability was discovered and patched in `src/aux.c`, where two `memcpy` calls in a path normalization function copied data into buffers without verifying sufficient capacity. An attacker capable of influencing the current working directory path — through deeply nested directories or crafted symlinks — could trigger heap corruption with potentially severe consequences. The fix introduces an integer overflow guard that ensures buffer allocation math cannot wrap

critical

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.

critical

Path Traversal Vulnerability Fixed in Hatch-Pet Scripts: A Deep Dive

A high-severity path traversal vulnerability was discovered and patched in the hatch-pet script suite, where unsanitized user input could allow attackers to read or overwrite sensitive files anywhere on the filesystem. The fix ensures that file paths are properly validated before use, preventing attackers from escaping the intended working directory. Understanding this class of vulnerability is essential for any developer working with file I/O and user-supplied input.

high

Path Traversal in Patch Utilities: How a Missing Validation Let Attackers Write Anywhere

A high-severity path traversal vulnerability (CWE-22) was discovered and fixed in the `patch` utility's input handling code, where filenames derived from diff headers were passed directly to file operations without sanitization. An attacker supplying a crafted patch file could have written arbitrary content to any location on the filesystem — including sensitive system files like `/etc/sudoers` or cron jobs. This post breaks down how the vulnerability works, why it's dangerous, and how to preven

high

Path Traversal Meets Dependency Vulnerabilities: A Two-Front Security Fix

A critical security update addresses both path traversal vulnerabilities in file system endpoints and a dependency issue with aiohttp's cookie handling. This fix demonstrates how modern applications face security threats on multiple fronts—from custom code vulnerabilities to third-party library weaknesses—and why comprehensive security auditing is essential.

critical

Heap Buffer Overflow in Audio Ring Buffer: How a Missing Bounds Check Could Crash Your App

A critical heap buffer overflow vulnerability was discovered in `audio_backend.c`, where the audio ring buffer's `memcpy` operations lacked bounds validation before writing PCM data. Without checking that incoming data sizes fell within the allocated buffer's capacity, a maliciously crafted audio file could corrupt adjacent heap memory, potentially enabling arbitrary code execution. The fix adds a concise pre-flight validation guard that rejects out-of-range write requests before any memory oper