Back to Blog
critical SEVERITY8 min read

How Path Traversal happens in Node.js FTP clients and how to fix it

A critical path traversal vulnerability (CVE-2026-27699) in the `basic-ftp` npm package (version 5.0.5) allowed attackers to overwrite arbitrary files on the host system by crafting malicious FTP server responses containing directory traversal sequences. The fix upgrades `basic-ftp` to version 5.3.1 and pins the dependency via a `package.json` override to ensure the patched version is used throughout the dependency tree.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-27699 is a critical path traversal vulnerability (CWE-22) in the `basic-ftp` npm package versions prior to 5.2.0, affecting Node.js applications that perform FTP downloads. A malicious FTP server can return filenames containing `../` sequences, causing `basic-ftp` to write files outside the intended download directory and overwrite arbitrary files on the host. The fix is to upgrade `basic-ftp` to version 5.3.1 (or at minimum 5.2.0) and add a `package.json` override to pin the patched version across the entire dependency tree.

Vulnerability at a Glance

cweCWE-22
fixUpgraded basic-ftp from 5.0.5 to 5.3.1 and pinned the version with a package.json override
riskArbitrary file overwrite on the host system via a malicious FTP server response
languageJavaScript / Node.js
root causebasic-ftp 5.0.5 did not sanitize server-supplied filenames before writing files to the local filesystem
vulnerabilityPath Traversal / File Overwrite

How Path Traversal Happens in Node.js FTP Clients and How to Fix It

The Incident: A Malicious FTP Server Can Overwrite Your Files

In the browserbase project, the automated security scanner Trivy flagged a critical vulnerability in package-lock.json: the project was depending on basic-ftp version 5.0.5, which is affected by CVE-2026-27699 — a file overwrite vulnerability caused by insufficient path sanitization during FTP downloads.

This is not a theoretical edge case. Any application that uses basic-ftp to download files from an FTP server it does not fully control is potentially exposed. The fix — upgrading to 5.3.1 and adding a package.json override — was straightforward, but understanding why the old version was dangerous is essential for any Node.js developer working with file I/O or remote data sources.


The Vulnerability Explained

What Is Path Traversal?

Path traversal (CWE-22) happens when an application constructs a filesystem path using data it received from an external source — a user, a server, a network response — without first sanitizing that data. The classic attack payload is the ../ sequence, which instructs the operating system to step up one directory level. Chain enough of them together and you can escape any intended directory.

In the context of an FTP client, the "external source" is the FTP server itself. When a client downloads a directory listing or a file, the server supplies the filenames. If the client trusts those filenames verbatim and writes them to disk, a malicious server can supply a name like:

../../../etc/cron.d/backdoor

And the client will dutifully write the file there — potentially overwriting a critical system file.

The Vulnerable Version: basic-ftp 5.0.5

The vulnerable dependency was locked at version 5.0.5 in package-lock.json:

// BEFORE (vulnerable)
"node_modules/basic-ftp": {
  "version": "5.0.5",
  "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
  "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
  "license": "MIT",
  "engines": {
    "node": ">=10.0.0"
  }
}

In basic-ftp 5.0.5, when downloading a remote directory recursively, the library constructs local file paths by joining the target download directory with the filename returned by the FTP server's LIST or MLSD command. The critical flaw is that the server-supplied filename was not checked for traversal sequences before being passed to the filesystem write operation.

Conceptually, the vulnerable pattern looked like this:

// Pseudocode representing the vulnerable behavior in basic-ftp 5.0.5
async function downloadFile(remoteFilename, localDir) {
  // remoteFilename comes directly from the FTP server's directory listing
  const localPath = path.join(localDir, remoteFilename); // ⚠️ NOT sanitized
  await fs.writeFile(localPath, fileContents);           // ⚠️ writes to attacker-controlled path
}

The path.join() call does collapse some traversal attempts, but it does not prevent all of them — and critically, it does not verify that the resolved path remains within localDir. A filename like ../../../../home/user/.ssh/authorized_keys will be resolved by path.join to a path that escapes the intended directory entirely.

A Concrete Attack Scenario

Imagine the browserbase application uses basic-ftp to connect to an external FTP server and download configuration files or assets. An attacker who controls that FTP server (or who can perform a man-in-the-middle attack on an unencrypted FTP connection) crafts a directory listing response containing:

-rw-r--r-- 1 ftp ftp 512 Jan 01 00:00 ../../../app/server.js

When basic-ftp 5.0.5 processes this listing and downloads the "file," it writes attacker-controlled content to ../../../app/server.js — potentially replacing the application's own server code with a backdoored version. On the next application restart, the attacker's code runs with full application privileges.

The severity rating of CRITICAL is well-earned: this is an unauthenticated, remote file overwrite primitive.


The Fix

Upgrading to basic-ftp 5.3.1

The fix involved two coordinated changes: updating the resolved package version in package-lock.json and adding a version override in package.json to ensure the pinned version propagates throughout the entire dependency tree.

package-lock.json — before and after:

// BEFORE (vulnerable)
"node_modules/basic-ftp": {
  "version": "5.0.5",
  "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
  "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="
}

// AFTER (patched)
"node_modules/basic-ftp": {
  "version": "5.3.1",
  "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
  "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="
}

package.json — the override addition:

// BEFORE
{
  "dependencies": {
    "@hyperbrowser/sdk": "^0.78.0",
    "dotenv": "^16.4.7",
    "puppeteer-core": "^24.31.0"
  }
}

// AFTER
{
  "dependencies": {
    "@hyperbrowser/sdk": "^0.78.0",
    "dotenv": "^16.4.7",
    "puppeteer-core": "^24.31.0"
  },
  "overrides": {
    "basic-ftp": "5.3.1"
  }
}

Why Two Files Had to Change

The package-lock.json change updates the resolved version that npm ci will actually install. But package-lock.json alone is not enough — if another dependency in the tree declares basic-ftp as a transitive dependency with a range that resolves to 5.0.5, npm could re-lock it to the vulnerable version on the next npm install. The "overrides" field in package.json is a hard instruction to npm: regardless of what any transitive dependency requests, always use basic-ftp 5.3.1. This two-file approach closes both the immediate and the future exposure.

What Changed Inside basic-ftp

In the patched versions (5.2.0+), basic-ftp validates that every server-supplied filename, after path resolution, remains within the intended local download directory. The fix introduces a check equivalent to:

// Representative of the fix introduced in basic-ftp 5.2.0+
function safePath(localDir, remoteFilename) {
  const resolved = path.resolve(localDir, remoteFilename);
  if (!resolved.startsWith(path.resolve(localDir) + path.sep)) {
    throw new Error(`Path traversal detected: ${remoteFilename}`);
  }
  return resolved;
}

This pattern — resolve first, then prefix-check — is the canonical defense against path traversal. It handles all forms of traversal including ../, URL-encoded variants, and null byte injection.


Prevention & Best Practices

1. Always Canonicalize Before Checking

Never rely on string matching alone to detect traversal sequences. Use path.resolve() to get the absolute, canonical path, then verify it starts with your intended base directory:

const path = require('path');

function isPathSafe(baseDir, userInput) {
  const base = path.resolve(baseDir);
  const target = path.resolve(baseDir, userInput);
  return target.startsWith(base + path.sep) || target === base;
}

2. Treat All Remote Data as Untrusted

FTP server responses, HTTP headers, ZIP file entries, tar archive member names — any filename that originates outside your application boundary must be treated as hostile input. This is especially true for FTP, which has no built-in integrity protection on unencrypted connections.

3. Pin Transitive Dependencies

Use "overrides" (npm 8.3+), "resolutions" (Yarn), or "overrides" (pnpm) to pin the versions of security-sensitive transitive dependencies. Don't rely on semver ranges to automatically pull in security patches — lock the version explicitly.

// npm package.json
{
  "overrides": {
    "basic-ftp": "5.3.1"
  }
}

4. Run Dependency Scanners in CI

Tools like Trivy, Snyk, and npm audit can catch known-vulnerable dependency versions before they reach production. Add them as a required CI step:

# In your CI pipeline
trivy fs --exit-code 1 --severity CRITICAL,HIGH .

5. Relevant Standards


Key Takeaways

  • basic-ftp 5.0.5 trusted server-supplied filenames verbatim — any application downloading files from an untrusted FTP server was vulnerable to arbitrary file overwrite.
  • path.join() is not a security control — it normalizes paths but does not prevent traversal out of a base directory. Always follow with a startsWith check on the resolved absolute path.
  • Updating package-lock.json is not enough — the "overrides" field in package.json is required to prevent npm from re-resolving the vulnerable version through transitive dependencies.
  • FTP is an inherently untrusted protocol — treat every piece of data returned by an FTP server, including filenames in directory listings, as potentially hostile input.
  • Trivy caught this before it reached production — static dependency scanning in CI is a cost-effective first line of defense against known-CVE supply chain risk.

How Orbis AppSec Detected This

  • Source: Filenames returned by a remote FTP server in directory listing responses (LIST/MLSD commands), which are entirely server-controlled and received over the network.
  • Sink: The filesystem write operation inside basic-ftp's recursive download logic, where the server-supplied filename was joined with the local target directory and passed directly to Node.js file I/O APIs.
  • Missing control: No validation that the resolved local path remained within the intended download directory — specifically, no path.resolve() + startsWith(baseDir) guard before writing.
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • Fix: Upgraded basic-ftp from 5.0.5 to 5.3.1 in package-lock.json and added an "overrides" entry in package.json to pin the patched version across the full dependency tree.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2026-27699 is a reminder that security vulnerabilities don't always live in the code you write — they hide in the dependencies you pull in and trust implicitly. A single unsanitized filename in an FTP client library became a critical file overwrite primitive that could compromise an entire host. The fix was a one-line version bump plus a dependency override, but the underlying lesson is durable: never trust externally supplied filenames, always canonicalize paths before writing, and keep your dependency scanner running in CI. Catching this class of issue automatically, before it ships, is exactly what supply chain security tooling is designed to do.


References

Frequently Asked Questions

What is a path traversal vulnerability?

A path traversal vulnerability occurs when an application uses unsanitized, user- or server-supplied input to construct a file path, allowing an attacker to navigate outside the intended directory using sequences like `../` and read or write arbitrary files.

How do you prevent path traversal in Node.js?

Always resolve and validate file paths using `path.resolve()` and confirm the result starts with the expected base directory before performing any file I/O. For third-party FTP libraries, keep dependencies up to date and pin safe versions in package.json.

What CWE is path traversal?

Path traversal is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).

Is input validation alone enough to prevent path traversal?

Input validation helps, but it is not sufficient on its own. Canonicalization (resolving symlinks and `..` segments with `path.resolve()`) followed by a prefix check against the intended base directory is the reliable defense.

Can static analysis detect path traversal?

Yes. Tools like Trivy (which flagged this exact CVE), Semgrep, and Snyk can identify vulnerable dependency versions and unsafe path construction patterns automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

critical

How path traversal happens in PHP virtual filesystem adapters and how to fix it

A critical path traversal flaw in `VirtualAdapter.php`'s `resolveMount()` method allowed attackers to escape mounted directory boundaries using sequences like `../../../etc/passwd`. The fix introduces `PathPolicy::normalizeRelative()` to sanitize the remaining path segment before it ever reaches the underlying storage adapter.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

high

How Trust-Prefix Bypass via Path Traversal Happens in Python Copier and How to Fix It

CVE-2026-53951 is a high-severity path traversal vulnerability in Copier 9.15.0 that allowed attackers to bypass trust-prefix checks and execute tasks without user confirmation. Upgrading to Copier 9.15.2 eliminates this attack vector by properly validating file paths before task execution.

critical

How Command Injection happens in Python subprocess calls and how to fix it

A critical command injection vulnerability was discovered in `spider/php/crawler.py` where the `PHPBridge.call()` method passed unvalidated external arguments directly to `subprocess.run()`. An attacker controlling the `spider_path` or `method` parameters could execute arbitrary PHP scripts or inject malicious method names. The fix adds strict input validation — requiring `method` to be a valid Python identifier and `spider_path` to resolve to an existing `.php` file — before any subprocess exec

critical

How Path Traversal in basic-ftp Leads to File Overwrite Attacks and How to Fix It

CVE-2026-27699 is a critical path traversal vulnerability in basic-ftp versions before 5.3.1 that allows attackers to overwrite arbitrary files on the system by crafting malicious file paths. This vulnerability was fixed by upgrading basic-ftp and enforcing strict version constraints across dependent packages. Understanding this attack and its mitigation is essential for developers using FTP libraries in production environments.

critical

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.