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 Orbis AppSec
Published May 28, 2026Reviewed June 3, 2026

Answer Summary

Path traversal (CWE-22) in a ZMODEM file receiver allowed attackers to write files to arbitrary filesystem locations by sending crafted filenames containing directory traversal sequences like `../../.ssh/authorized_keys`. The vulnerability occurred because the receiver used sender-supplied filenames directly in file operations without validation. The fix strips all path separators and validates that the resulting filename is safe, using basename extraction to ensure files can only be written to the intended download directory.

Vulnerability at a Glance

cweCWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
fixStrip path separators and validate filenames using basename extraction before file operations
riskRemote attacker can overwrite critical system files like SSH authorized_keys
languageImplementation-agnostic (affects ZMODEM protocol implementations)
root causeAccepting sender-supplied filenames without stripping directory components
vulnerabilityPath Traversal in ZMODEM File Receiver

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.

Frequently Asked Questions

What is path traversal in ZMODEM file receivers?

Path traversal in ZMODEM occurs when a file receiver accepts filenames from the sender without validation, allowing directory traversal sequences like `../` to write files outside the intended download directory. This can lead to overwriting critical system files.

How do you prevent path traversal in ZMODEM implementations?

Extract only the basename (filename without directory components) from sender-supplied filenames, validate that the result contains no path separators, and construct the full path by joining the validated filename with a trusted download directory. Never use sender-supplied paths directly.

What CWE is path traversal in file receivers?

CWE-22: Improper Limitation of a Pathname to a Restricted Directory, also known as "Path Traversal" or "Directory Traversal." This vulnerability occurs when software uses external input to construct a pathname without properly neutralizing special elements.

Is checking for `../` sequences enough to prevent path traversal?

No. Attackers can use absolute paths (starting with `/`), URL-encoded sequences (`%2e%2e%2f`), backslashes on Windows (`..\..\`), or Unicode variants. The only safe approach is to extract the basename and validate it contains no path separators whatsoever.

Can static analysis detect path traversal in ZMODEM implementations?

Yes. Static analysis tools can identify when user-controlled filenames are used in file operations without proper sanitization. Tools look for tainted data flows from network input to filesystem operations without intervening validation or basename extraction.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #67

Related Articles

high

How URL-Encoded Path Traversal happens in Python nltk.data.load() and how to fix it

CVE-2026-54293 is a high-severity path traversal vulnerability in NLTK's `nltk.data.load()` function that allows attackers to read arbitrary local files by supplying URL-encoded path sequences. The fix pins NLTK to version 3.10.0 or later via a constraint dependency in `pyproject.toml`, preventing the vulnerable version from being resolved transitively through `rouge-score` and `lm-eval`. Because this project is a web service, the vulnerability was directly exploitable by remote attackers withou

high

How path traversal happens in Ruby YARD server and how to fix it

A high-severity path traversal vulnerability (CVE-2026-41493) in YARD versions prior to 0.9.42 allowed attackers to read arbitrary files from servers running `yard server`. This fix upgrades the yard gem from 0.9.26 to 0.9.42 in the Gemfile and Gemfile.lock, closing a dangerous information disclosure vector that could expose configuration files, credentials, and source code.

critical

How path traversal happens in Python os.path and how to fix it

A critical path traversal vulnerability in the TRL backend allowed attackers to read arbitrary system files like `/etc/passwd` and `/proc/self/environ` through the gRPC fine-tuning API. The `_do_training` method passed user-controlled `dataset_source` directly to `os.path.exists()` and `load_dataset()` without validation. The fix implements strict directory containment checks using `os.path.realpath()` to ensure all file operations stay within allowed directories.

medium

How path traversal happens in C file extraction and how to fix it

A path traversal vulnerability in the borpak archive extraction tool allowed attackers to write files to arbitrary locations on the filesystem by crafting malicious .pak archives with `../` sequences in filenames. This medium-severity issue in `tools/borpak/source/borpak.c` could enable system compromise through overwriting critical files like `.bashrc` or cron jobs. The fix implements path validation to ensure extracted files never escape the intended extraction directory.

high

How path traversal in open() happens in Python and how to fix it

A high-severity path traversal vulnerability was discovered in `tool/update-doc.py`, where user-controlled input was passed directly to Python's `open()` function without sanitization. This flaw could allow an attacker to read arbitrary files on the server by manipulating the file path. The fix ensures that file paths are validated and restricted to an intended directory before being opened.

critical

How command injection happens in Go ffmpeg wrappers and how to fix it

A critical command injection vulnerability was discovered in `drivers/local/util.go` where user-influenced file paths were passed directly to `ffmpeg.Input()` without any sanitization. Because many ffmpeg wrapper libraries construct shell command strings under the hood, an attacker could embed shell metacharacters in a file path to execute arbitrary OS commands with server-level privileges. The fix introduces a `sanitizeFilePath()` function that validates paths are absolute, clean, and point to