New MCP server — Bring vulnerability intelligence into your AI
Back to feed

CVE-2026-44240

NVDCIRCLENISA

EUVD-2026-29840 · Published May 6, 2026

basic-ftp allows a malicious FTP server to cause client-side denial of service via unbounded multiline control response buffering

Summary

basic-ftp is vulnerable to client-side denial of service when parsing FTP control-channel multiline responses.

A malicious or compromised FTP server can send an unterminated multiline response during the initial FTP banner phase, before authentication. The client keeps appending attacker-controlled data into FtpContext._partialResponse and repeatedly reparses the accumulated buffer without enforcing a maximum control response size.

As a result, an application using basic-ftp can remain stuck in connect() while memory and CPU usage grow under attacker-controlled input. This can lead to process-level denial of service, container OOM kills, worker restarts, queue backlog, or service degradation in applications that automatically connect to FTP endpoints.


Details

Root cause

The root cause is that incomplete FTP multiline control responses are buffered without an upper bound.

FtpContext stores incomplete control-channel data in _partialResponse:

https://github.com/patrickjuchli/basic-ftp/blob/50827c73ca6c1d786c97276e47be8a33d0f2277d/src/FtpContext.ts#L63-L64

Incoming control-channel data is handled in _onControlSocketData. The implementation concatenates the previous incomplete response with the new chunk, parses the entire accumulated string, and stores parsed.rest back into _partialResponse:

https://github.com/patrickjuchli/basic-ftp/blob/50827c73ca6c1d786c97276e47be8a33d0f2277d/src/FtpContext.ts#L328-L340

The relevant flow is:

completeResponse = this._partialResponse + chunk
parsed = parseControlResponse(completeResponse)
this._partialResponse = parsed.rest

There is no maximum size check before concatenating, before parsing, or before storing parsed.rest.

The parser accepts incomplete multiline responses and returns the entire unterminated multiline group as rest:

https://github.com/patrickjuchli/basic-ftp/blob/50827c73ca6c1d786c97276e47be8a33d0f2277d/src/parseControlResponse.ts#L15-L43

If a server starts a multiline FTP response:

220-malicious banner starts

but never sends the terminating line:

220 ready

then parseControlResponse() treats the accumulated multiline data as incomplete and returns it as rest.

Because _onControlSocketData() feeds _partialResponse + chunk back into the parser on every new data event, the client repeatedly reparses a growing attacker-controlled buffer. This creates both memory growth and increasing parsing work.

Why this is security-relevant

The vulnerable component is a client library. The attacker does not need to authenticate to the victim system and does not need valid FTP credentials.

The attack occurs automatically when an application using basic-ftp connects to a malicious or compromised FTP server. The malicious response is sent as the FTP server banner before login. No additional user interaction is required after the application initiates a normal FTP connection.

This is realistic for applications that use FTP for:

  • scheduled imports or exports
  • customer-provided FTP endpoints
  • backup or synchronization jobs
  • CI/CD artifact mirroring
  • document ingestion pipelines
  • legacy business integrations

In those environments, one malicious or compromised FTP endpoint can cause the Node.js process using basic-ftp to consume excessive memory and CPU or remain stuck in a pending connection state.


Proof of Concept

The PoC uses a local malicious FTP server that accepts a victim connection and sends an unterminated multiline FTP banner. The banner starts with 220-, but the server never sends the required terminating 220 line.

Reproduction steps

From the root of the basic-ftp project:

npm ci
npm run buildOnly

poc_control_parser_direct.js

CHUNKS=1000 node poc_control_parser_direct.js | tee poc-results/parser_direct_1000.log

parser_direct_1000.log

Run the end-to-end malicious FTP server PoC:

poc_control_multiline_dos.js

CHUNK_SIZE=8192 CHUNKS=1000 DELAY_MS=1 node poc_control_multiline_dos.js | tee poc-results/control_multiline_dos_1000.log

control_multiline_dos_1000.log

Observed result: parser-only PoC
[basic-ftp parseControlResponse incomplete multiline DoS]
Input fed: 7.81 MiB
Retained rest: 7.81 MiB
Initial rss/heap: 54.77 MiB 3.69 MiB
Final   rss/heap: 141.64 MiB 80.77 MiB

This shows that parseControlResponse() retained the full unterminated multiline response as rest.

The retained buffer grew to 7.81 MiB. Heap usage increased from 3.69 MiB to 80.77 MiB, and RSS increased from 54.77 MiB to 141.64 MiB.

Observed result: end-to-end malicious FTP server PoC
[server] listening on 127.0.0.1:34429
[server] victim connected
[progress] chunks=850 sent=6.6 MiB partialResponse=6.6 MiB heapUsed=227.5 MiB rss=292.4 MiB
[progress] chunks=900 sent=7.0 MiB partialResponse=7.0 MiB heapUsed=213.1 MiB rss=278.0 MiB
[final-before-close] chunks=1000 sent=7.8 MiB partialResponse=7.8 MiB heapUsed=82.1 MiB rss=146.8 MiB
[result] client connect() is still pending because the multiline response never terminated

Only 7.8 MiB of malicious control-channel data was sent. The client retained 7.8 MiB in _partialResponse, showed large memory spikes, and remained pending inside connect() because the multiline response was never terminated.


Expected behavior

The client should enforce a maximum size for incomplete FTP control responses. If the accumulated multiline response exceeds a safe limit, the client should close the connection and reject the active task with an error.

The client should not allow a remote FTP server to make _partialResponse grow without bound.


Actual behavior

A malicious FTP server can keep the client in a pending connection state by sending an unterminated multiline control response. basic-ftp continues buffering and reparsing the accumulated data without a maximum response size.


Impact

A malicious or compromised FTP server can cause denial of service in applications using basic-ftp.

Possible real-world impact includes:

  • Node.js process memory exhaustion
  • container OOM kill
  • worker crash or restart loop
  • event loop CPU pressure due to repeated reparsing
  • stuck FTP jobs
  • queue backlog in scheduled import/export systems
  • degraded availability of services relying on automated FTP ingestion

Threat model

The attacker controls, compromises, or can impersonate an FTP server that a victim application connects to.

Examples:

  1. A SaaS application allows customers to configure external FTP endpoints for automated imports.
  2. A backend job periodically pulls files from partner FTP servers.
  3. A document ingestion pipeline connects to FTP endpoints supplied by external users.
  4. A legacy integration uses FTP for scheduled synchronization.
  5. A build or deployment pipeline mirrors artifacts from an FTP server.

In each case, the victim application initiates a normal FTP connection. The malicious server sends an unterminated multiline banner before authentication. The vulnerable client then buffers and reparses the response indefinitely.

No FTP credentials are required for exploitation because the attack happens before login.


Suggested fix

Introduce a maximum control response buffer size, especially for incomplete multiline responses.

Recommended changes:

  • Add a maxControlResponseBytes or maxControlResponseLength limit.
  • Enforce the limit before or immediately after appending new control-channel data.
  • Close the connection and reject the active task when the limit is exceeded.
  • Add regression tests for unterminated multiline responses.

Example defensive logic:

if (completeResponse.length > maxControlResponseLength) {
    closeWithError(new Error("FTP control response exceeded maximum allowed size"))
}

A regression test should verify that a response beginning with 220- and never terminating with 220 is rejected after the configured size limit instead of being retained indefinitely.


Suggested regression test scenario

A test server should:

  1. Accept a client connection.
  2. Send an FTP multiline response opener such as 220-malicious banner\r\n.
  3. Continue sending additional lines without ever sending the terminating 220 line.
  4. Verify that the client rejects the connection once the configured response-size limit is exceeded.
  5. Verify that _partialResponse does not grow without bound.

Credit request

If you publish an advisory or assign a CVE, please credit me as:

Ali Firas (thesmartshadow) - https://www.smartshadow.dev

Technologies

Basic-Ftp Basic-Ftp

Weaknesses

CWE-770CWE-400

CVSS scores

  • v3.17.5CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

MITRE ATT&CK

Inferred

Where this vulnerability fits in an attack

Initial Access Impact

References

Potentially impacted assets

See if this affects your attack surface

Latest trending attack

Criticalyesterday

Google Brotli decoder - Remote Code Execution via compressed requests (Exploitation Check)

This control is the second step of the Patrowl response to CVE-2026-111497 (Node2Shell). It complements the first control of 22 October 2026, which focused on the Node.js runtime, and does not replace it. Why a second control CVE-2026-111497 is an out-of-bounds write in the Google Brotli decoder, the library that decompresses Brotli ("br") data. It was first published as a Node.js vulnerability, because Node.js bundles this decoder. Later publications showed that the flaw is in the library itself. Every product that embeds an affected version of the decoder is concerned, whatever its language or vendor. A public proof of concept demonstrates code execution. Any server that decompresses request bodies sent with "Content-Encoding: br" exposes the decoder to unauthenticated attackers. Known cases include Express 5 and NestJS 11 on Node.js, Koa and Fastify setups, Bun, Envoy and Istio with request decompression enabled, and any application or reverse proxy that decodes Brotli request bodies with an affected library. The flaw also works in the other direction. A malicious server can compromise an unpatched client that decodes a Brotli response, for example Node.js fetch(), Python clients (requests, httpx, aiohttp, Scrapy) with the Brotli or brotlicffi package, or libcurl builds with Brotli support. What this control checks Patrowl sends small, valid Brotli-compressed requests to each asset and observes whether the server decodes them. The check does not depend on the technology: it detects the behaviour, whatever the stack behind it. These requests are what any legitimate client could send. They never reach the vulnerable code path and cannot affect the availability of the service. How to read the result Potentially impacted means the asset decodes Brotli request bodies: the attack surface is exposed. It does not confirm that the asset is vulnerable. The version of the Brotli decoder in use is not visible from the outside. This check covers applications that parse request bodies on their root path. A Not impacted result does not rule out exposure on other routes, nor on the client side. Checking the versions on the servers remains the only exhaustive verification. Affected and fixed versions Google Brotli 1.0.3 to 1.2.0 are affected, fixed in 1.0.10, 1.1.1 and 1.2.1. Fixed releases of the main products embedding it: Node.js 22.23.4, 24.22.1 and 26.12.1 (end-of-life lines 20.x and older and 25.x receive no fix), Bun 1.4.4, Envoy 1.37.8, 1.38.6, 1.39.3 and 1.40.0, Istio 1.29.9, 1.30.6 and 1.31.2, Python Brotli 1.2.1 and brotlicffi 1.2.1.0. Products that link against the system library depend on the Brotli package of the Linux distribution. Recommendations Update every component that embeds the Brotli decoder (only real fix). Start with the assets flagged by this control. Restart the processes after the update. Rebuild containers, single-executable applications and appliances that bundle an affected runtime or library. Reduce the exposure until the update is deployed. Reject requests with "Content-Encoding: br" at the reverse proxy, load balancer or WAF, case-insensitively and including lists such as "gzip, br". On Envoy and Istio, disable request decompression for Brotli. Review outbound calls. List the features that fetch content from servers you do not control, and patch the services that host them first.

Take 15 minutes to discover our platform with our experts

Patrowl Intel platform screenshot