CVE-2026-126346: basic-ftp: Uncaught exception crashes the Node.js process when a password (or resumed-download path) contains CR/LF

Published Nov 12, 2026
·
Updated

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

2 affected componentsFixes available
npm/basic-ftp<=6.2.2
6.2.3
npm/basic-ftp<6.2.3
6.2.3

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/basic-ftp to a version that resolves this vulnerability.

    Fixed in 6.2.3
  2. Upgrade

    Upgrade basic-ftp to a version that resolves this vulnerability.

    Fixed in 6.2.3

Event History

Nov 12, 2026
CVE Published
via OSV·09:42 PM
Data Sourced
via OSV·09:42 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·10:19 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·10:23 PM
Data Sourced
via GitHub·10:23 PM
DescriptionSeverityWeaknessAffected Software
Data Sourced
via OSV·10:23 PM
Affected Software

Frequently Asked Questions

1

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.

2

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.

3

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.

4

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203