CVE-2026-126346: basic-ftp: Uncaught exception crashes the Node.js process when a password (or resumed-download path) contains CR/LF
Summary
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.
Details
The CRLF guard (FTPContext.send, src/FtpContext.ts ~L233) correctly refuses to send commands containing \r, \n or \0: ts 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.)
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.
Proof of concept
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:
js 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.
Impact
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.
Other sources
basic-ftp is an FTP client for Node.js. Prior to 6.2.3, FTPContext.send() throws when a command contains carriage return, line feed, or NUL characters, but calls made from response handlers execute synchronously inside the control socket's data event without an exception boundary. In Client.login(), an attacker-influenced password reaches the PASS command after a server sends a 331 response, and in a resumed download an attacker-influenced remote path reaches the RETR command after a 350 response to REST. The resulting exception does not reject client.access() or downloadTo(), escapes ordinary promise error handling, and terminates the Node.js process, while the validation guard still prevents command injection. This issue is fixed in version 6.2.3.
— NVD
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/basic-ftpto a version that resolves this vulnerability.Fixed in 6.2.3 - Upgrade
Upgrade
basic-ftpto a version that resolves this vulnerability.Fixed in 6.2.3
Event History
Frequently Asked Questions
What conditions are required to trigger the process crash?
An attacker must be able to cause the application to pass attacker-controlled input as the FTP password, or to use an attacker-controlled remote path with downloadTo(). The password path is reached after an FTP server responds with a 331 response to the USER command; the supplied value must contain CR, LF, or NUL.
Is this an FTP command-injection issue?
The control-character validation rejects CR, LF, and NUL rather than sending them as FTP commands. The issue is that the resulting exception is uncaught in the affected response-handler paths, causing the Node.js process to terminate.
What can be done if updating is not immediately possible?
Do not pass untrusted values as FTP passwords or remote paths used by downloadTo(). Validate or reject any password or path containing carriage return, line feed, or NUL before calling the affected operations.
How can I determine whether an application is exposed?
Review whether it uses npm/basic-ftp and supplies FTP credentials or downloadTo() remote paths from request-controlled or other untrusted input. Applications that do so can be tested by ensuring those values are rejected before they reach the library when they contain CR, LF, or NUL.