BASIC-FTP · CVE-2026-126346 · Package
FIX → 6.2.3
basic-ftp throws an uncaught exception inside its socket data handler when the FTP password or a resumed-download remote path contains CR, LF or NUL characters. An attacker who controls that string can terminate the Node.js process with one request, unless the app has a global uncaughtException handler.
first public Oct 30, 2026 · 2 sources
Check it with your coding agent
It reads this page and tells you if your code or app is affected.
Applications that use basic-ftp and pass untrusted or user-influenced values as the FTP password or as the remote path of a resumed download, such as apps where users enter their own FTP credentials or backup tools driven by user paths. Apps that only use fixed, server-controlled credentials and paths are not exposed to this crash path.
Not affected if fixed-credentials, no-user-input-in-paths
Who cares basic-ftp, ftp, node
npm ls basic-ftp to find the installed version and whether it is pulled in transitively.password in client.access() or as the remote path in downloadTo().process.on("uncaughtException") handler in the app.Upgrade to 6.2.3
Client.login() calls FTPContext.send() from inside the control-socket data event handler. send() throws on CR, LF or NUL, and nothing catches the throw. A password containing \r\n therefore raises an uncaught exception and terminates the whole Node.js process. It does not reject the returned promise. Any application that passes untrusted input as the FTP password can be crashed remotely with one request. The same pattern exists in the REST/RETR path of downloadTo() when the remote path contains control characters.
The CRLF guard (FTPContext.send, src/FtpContext.ts ~L233) correctly refuses to send commands containing \r, \n or \0:
send(command: string) {
if (/[\r\n\0]/.test(command)) {
throw new Error(`Invalid command: Contains control characters. (${command})`);
}
That works when send() is called from handle(), because it runs inside a Promise executor and the throw becomes a rejection. But in two places send() runs inside a response handler, which is called synchronously from the socket’s data event:
Client.login() (src/Client.ts ~L246-248): after the server answers 331 to USER, the handler calls this.ftp.send("PASS " + password). The USER command was already sent and is valid, so the password is only checked at this point. The call chain is socket.on("data") → _onControlSocketData → _passToHandler → responseHandler → send() → throw. No try/catch exists anywhere in that chain, so it becomes an uncaughtException. downloadTo() (src/transfer.ts ~L368-370): when startAt > 0, the initial command is REST n, which contains no user-controlled text. When the server answers 350, the handler calls config.ftp.send("RETR " + config.remotePath). This remotePath was never checked before. A path with a control character throws in the data handler in the same way. (Found by code reading, not reproduced.)
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
InfraStack checks it against your stack and answers on your report, with the evidence and the fix.
6.2.3 (affected: <= 6.2.2).Because the exception happens during data event dispatch, it can’t be handled with try/catch or .catch() around client.access() or downloadTo(). The application only survives if it has a global process.on("uncaughtException") handler.
Tested on Node.js v22.22.0 against basic-ftp 6.2.2 (built from source). It uses a minimal mock FTP server on localhost:
const net = require("net")
const ftp = require("basic-ftp")
const srv = net.createServer(s => {
s.write("220 hi\r\n")
s.on("data", d => {
const c = d.toString()
if (c.startsWith("USER")) s.write("331 pw\r\n")
else if (c.startsWith("PASS")) s.write("230 ok\r\n")
else s.write("200 ok\r\n")
})
})
srv.listen(0, async () => {
const c = new ftp.Client(2000)
try {
await c.access({
host: "127.0.0.1",
port: srv.address().port,
user: "bob",
password: "x\r\nDELE important" // attacker-controlled value
})
} catch (e) {
console.log("rejected:", e.message) // never reached
}
console.log("handled gracefully") // never reached
process.exit(0)
})
Result: the process terminates with an uncaught exception and neither catch nor the final console.log runs:
Error: Invalid command: Contains control characters. (PASS x
DELE important)
at FTPContext.send (dist/FtpContext.js:189:19)
at Object.responseHandler (dist/Client.js:186:26)
at FTPContext._passToHandler (dist/FtpContext.js:322:24)
at FTPContext._onControlSocketData (dist/FtpContext.js:312:18)
at Socket.<anonymous> (dist/FtpContext.js:133:44)
The server in the PoC is benign. Any normal FTP server that replies 331 to USER triggers the same crash.
Denial of service (uncaught exception, process termination), CWE-248.
It affects applications that pass untrusted or user-influenced values as the FTP password, or as the remote path of a resumed download. Examples are web apps or services that let users enter their own FTP credentials, or file-transfer or backup tools driven by user-supplied paths. One request crashes the whole Node.js process and every other client it serves. It needs no authentication to the FTP server and no special server behaviour. The attacker only needs control over the string. Confidentiality and integrity are not affected, and the CRLF guard still blocks command injection.
Suggested fix:
In FTPContext._passToHandler(), wrap the handler call in a try/catch that rejects the task and calls closeWithError(err). Validate password in login(), and remotePath in _downloadToStream(), before any I/O and reject early.
This page as .md · .json · CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.