Library / SDK
patrickjuchli/basic-ftp avatar
patrickjuchli/basic-ftp

basic-ftp: a Node.js FTP client with FTPS and a bounce attack guard

FTP client for Node.js, supports FTPS over TLS, passive mode over IPv6, async/await, and Typescript.

736 stars105 forksJavaScriptMIT

At a glance

What is it?
basic-ftp is an FTP client library for Node.js with a Promise based API, TLS support for FTPS, passive mode over IPv6, and methods that operate on whole directories. Its documentation opens with an advisory asking you to use HTTPS or SFTP instead, and one of its defaults refuses the server side redirect that FTP bounce attacks rely on.
Who is it for?
basic-ftp fits a Node.js service that has to talk to a legacy FTP server it does not control, and that wants async and await, TLS, and directory level operations rather than a shell out to a command line client. It does not fit a new integration, because the library's own advisory says FTP is an old protocol with reliability issues and no security on its own, and it does not fit a workflow that needs active mode.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The advisory comes first

The README opens with a section titled Advisory, and it is the first substantive thing a reader sees.

The advice is to prefer alternative transfer protocols like HTTPS or SFTP over SSH. The reason given is that FTP is an old protocol with some reliability issues, and the instruction is to use the library when you have no choice and need to use FTP. Then the security half: try to use FTPS, which is FTP over TLS, whenever possible, because FTP alone does not provide any security.

That ordering matters, and it is a good sign about the project. A library that opens by telling you when not to use it is one that expects to be judged on whether it does its job well, not on whether it gets used.

The second line of the README is the scope. This is an FTP client library for Node.js, it supports FTPS over TLS, passive mode over IPv6, it has a Promise based API, it offers methods to operate on whole directories, and active mode is not supported.

The dependency situation is unusually clean for a protocol library of this kind. Node 10.0 or later is the only dependency, with no third party packages at all, and the project is written in TypeScript with its own type declarations. The install is a single command.

bash
npm install basic-ftp

The project is MIT licensed, the last push was on 2026-10-09, and the recent releases are v6.2.3 from 2026-10-09, 6.2.2 from 2026-10-04, and 6.2.1 from 2026-08-27, so it moves quickly within a major version.

The client, the access call, and the defaults

The example in the README is the whole shape of the library, and it is worth reading closely.

js
async function example() {
    const client = new Client()
    client.ftp.verbose = true
    try {
        await client.access({
            host: "myftpserver.com",
            user: "very",
            password: "password",
            secure: true
        })
        console.log(await client.list())
        await client.uploadFrom("README.md", "README_FTP.md")
        await client.downloadTo("README_COPY.md", "README_FTP.md")

There is a note about the ES module form, where the import is the named import of the Client class, and a comment above the example that the FTP protocol does not allow multiple requests running in parallel. That last point is a property of the protocol rather than of the library, and it is the kind of thing that determines how you write the code around it.

The constructor takes a timeout in milliseconds and an options object. The timeout is used for any connection made, zero disables timeouts, and the default is 30 seconds. The options are the connection details, and the defaults are worth knowing because they are permissive: the host defaults to localhost, the port to 21, the user to anonymous, and the password to guest.

The secure option takes a boolean or the string implicit. It is explicit FTPS over TLS by default being false, and the implicit value is there for legacy implicit FTPS, which negotiates TLS on the control connection itself rather than upgrading it. The secureOptions object is the same set of options as the tls connect call in Node, so you can pass a certificate authority or a client certificate for mutual TLS.

The access method returns the response of the initial connect command, and it applies the default session settings, which are the transfer type, the structure, the protection buffer size, and the protection level, all set to their protected defaults. Because it is an instance method it can be called repeatedly, and each call resets the client with a new connection, which is also how you reopen a client that was closed by an error.

The bounce attack guard, and the one option worth understanding

There is a single option on the client, and the reason it exists is the most security-conscious thing in the library.

The option is allowSeparateTransferHost, a boolean. The FTP specification makes it possible for a server to tell the client to use a different IP address for file transfers than for the initial control connection. The README calls that a potential vector for FTP bounce attacks, so by default it is false, and the library will throw an error if a server tries to redirect transfers to a different host. The advice is to set it to true only if you are connecting to a server that legitimately requires it.

It is worth understanding what the attack is, because the default only makes sense once you do. The control connection is to a server you chose. The data connection, in a normal FTP transfer, is also to that server. But the server can instruct the client to connect somewhere else entirely for the data channel, and the client, which trusts the server it is already talking to, does it. That is the bounce, and it is a way to make a trusted client inside your network connect to an arbitrary host, often one that is only reachable from where your client sits.

So the default is: the library trusts the server for the control channel and refuses to trust it for the address of the data channel. The exception exists because some deployments put a load balancer or a proxy in front of the file service, and the server legitimately advertises a different transfer host. That is a real case, and the option is the escape hatch, but it is the kind of escape hatch that should be flipped deliberately rather than during debugging.

The rest of the safety story is the advisory. FTP alone sends the credentials in the clear, and the library supports TLS in two flavours precisely so that there is a secure option available for the cases where the protocol is unavoidable.

What the timeout does, and what it does not

The timeout semantics are documented carefully, and the detail is the kind that saves an afternoon.

The timeout applies to the server: a transfer fails if the server stops making progress for that long. It does not limit how long your own streams may take. A download piped into a slow destination, or an upload fed by a slow source, can hold up a transfer for as long as it needs without running into a timeout. And if you want to limit that as well, the documentation says to do it in your own code.

That is a defensible design, because the two failures are different. A server that has stopped sending is broken, and a timeout is the right response. A consumer that is slow is not broken, and a timeout there would produce a spurious error on a healthy transfer.

The consequence for a user is that a stalled server produces an error, while a slow pipeline produces a long running promise, and you need your own deadline for the second case. The default of 30 seconds is also worth thinking about in context, because a transfer from a slow large server over a slow link can legitimately have gaps longer than that, and the timeout is a liveness check rather than a total duration limit.

The client also closes itself on a timeout or a connection error, and the closed property reports whether the client is connected. The close method closes the client and any open connection, and after it the client cannot be used again, so you have to reconnect with access to continue any work.

The method surface, and the directory helpers

The API reference is long, and it is worth grouping.

The raw escape hatches are send, which sends an FTP command and returns the first response, and sendIgnoringError, which does the same but ignores an FTP error response, while any other error or timeout still rejects the promise.

The navigation calls are cd for changing the working directory and pwd for reading it. The listing call is list, which lists files and directories in the current directory or at a path, and the library currently supports only MLSD, Unix, and DOS directory listings, with the file details described in a separate type in the source.

The metadata calls are lastMod for the last modification time of a file, which might not be supported by your server and can throw, and size for the size in bytes. The mutation calls are rename, which can also move a file if you give it full paths, and remove.

The feature probe is features, which returns a map of supported features with the FTP command as the key, and it returns an empty map rather than an error if the server does not support the request, which is a friendlier behaviour than a rejection and lets you degrade rather than fail.

The transfer calls are the ones that matter most. uploadFrom takes a readable stream or a local path and a remote path, overwriting an existing file, with options for local start and local end inclusive so you can upload part of a file. appendFrom does the same but appends to an existing file, and the documentation gives the reason as resuming a failed upload, which is the operation that FTP's own resume support makes awkward.

The directory helpers are the reason the library is more than a protocol wrapper.

js
await client.ensureDir("my/remote/directory")
await client.clearWorkingDir()
await client.uploadFromDir("my/local/directory")

The first makes sure a remote path exists, creating all directories as necessary. The second makes sure the working directory is empty. The third uploads the contents of a local directory. Three calls replace a sequence of listing, deleting, and uploading, which is the tedious part of any FTP integration.

And for when something goes wrong, there is a verbose mode that logs all communication with the server, set with the verbose flag on the underlying ftp object.

How it is built and tested

The package manifest is small, and the scripts show how the project is verified.

The package is basic-ftp at version 6.2.3, with both the main entry and the types entry pointing at the built output in the dist directory, and the published files being only that directory. The engines field requires Node 10 or later, which matches the statement in the README that Node 10 is the only dependency.

The test script is the whole verification, and it is a sequence rather than a single command. The prepublish script cleans the output, lints, compiles with the TypeScript compiler, and then runs the test files with the Node test runner, and the test script is that same sequence. There is a watch mode for the tests, a separate integration test target, and a development target that compiles in watch mode.

The integration test target is the detail that tells you how seriously this is taken, because the dev dependencies include testcontainers. That means the integration tests can start a real FTP server in a container and talk to it, rather than asserting against a mock that behaves the way the author hopes a server behaves. For a protocol library, that is the difference between a test suite that proves interoperability and one that proves the code runs.

The linting is modern, with an ESLint flat configuration file and a legacy one, the TypeScript version is a current major, and the source is TypeScript throughout with the type declarations shipped to consumers.

The repository also carries a security policy and a notice file, which is the norm for a library that implements TLS, and a changelog and contributing guide.

Editorial conclusion

basic-ftp fits a Node.js service that has to talk to a legacy FTP server it does not control, and that wants async and await, TLS, and directory level operations rather than a shell out to a command line client. It does not fit a new integration, because the library's own advisory says FTP is an old protocol with reliability issues and no security on its own, and it does not fit a workflow that needs active mode. Verify first that your server supports the listing formats the library parses, since only MLSD, Unix, and DOS listings are supported, that you leave allowSeparateTransferHost at its default false unless the server genuinely requires the redirect, and that you treat the client timeout as a server liveness check rather than as a limit on your own transfers.

Frequently asked questions

Does basic-ftp support FTPS and IPv6?

Yes. It supports FTPS over TLS, in both explicit and legacy implicit form, and passive mode over IPv6. Active mode is not supported, and the README advises using FTPS whenever possible because plain FTP provides no security.

What is the allowSeparateTransferHost option in basic-ftp?

It allows a server to redirect file transfers to a different IP address than the control connection, which is a potential vector for FTP bounce attacks. It is false by default, and the library throws an error if a server tries to redirect transfers elsewhere, so you should only enable it for a server that legitimately requires it.

How does the basic-ftp timeout behave during a slow transfer?

The timeout applies to the server rather than to your own streams. A transfer fails if the server stops making progress for that long, but a download piped into a slow destination can hold up a transfer for as long as it needs, and the documentation says to limit that in your own code if you need to.

Can basic-ftp upload a whole directory?

Yes. There are helpers that make sure a remote directory exists by creating all directories as necessary, make sure the working directory is empty, and upload the contents of a local directory, which replaces a listing, deleting, and uploading sequence with three calls.

Which directory listing formats does basic-ftp support?

Only MLSD, Unix, and DOS directory listings. The file details returned are described in a separate type in the source, and the features method can be used to ask the server what it supports, returning an empty map rather than an error when the server does not answer.

Official sources

  1. Issues
  2. License: MIT
  3. patrickjuchli/basic-ftp on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/patrickjuchli-basic-ftp.svg)](https://hysenlabs.com/projects/patrickjuchli-basic-ftp)

Was this useful?