From f55e27097c701e5578af41bce3c2ae27d8b695e9 Mon Sep 17 00:00:00 2001 From: t Date: Fri, 10 Jul 2026 13:58:07 -0700 Subject: [PATCH 1/2] fix(deps): override basic-ftp to 5.3.1 to clear CVE-2026-39983 + 3 sibling HIGH advisories basic-ftp reaches the tree only transitively: puppeteer-core > @puppeteer/browsers > proxy-agent > pac-proxy-agent > get-uri > basic-ftp Versions <= 5.3.0 carry four HIGH advisories, all fixed in 5.3.1: - GHSA-chqc-8p9q-pq6q (CVE-2026-39983) FTP command injection via CRLF - GHSA-6v7q-wjvx-w8wg incomplete CRLF protection (USER/PASS + MKD bypass) - GHSA-rpmf-866q-6p89 DoS via unbounded multiline control-response buffering - GHSA-rp42-5vxx-qpwr DoS via unbounded memory in Client.list() Pin via a bun `overrides` entry rather than a phantom direct dependency. Overriding forces every basic-ftp in the tree to 5.3.1, including get-uri's nested copy; `bun audit` then reports zero basic-ftp advisories (total 37 -> 33, HIGH 13 -> 9). A direct-dependency bump leaves get-uri/basic-ftp at the vulnerable version and audit still flags all four. Co-Authored-By: Claude Opus 4.8 (1M context) --- bun.lock | 5 ++++- package.json | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 7a1833e943..90b8fddf46 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,9 @@ }, }, }, + "overrides": { + "basic-ftp": "5.3.1", + }, "packages": { "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.117", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.117", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.117" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pVBss1Vu0w87nKCBhWtjMggSgCh6GVUtdRmuE58ZvXv0E2q0JcnUCQHehmn92BAW0+VCwPY8q/k7uKWkgwz/gA=="], @@ -201,7 +204,7 @@ "bare-url": ["bare-url@2.4.0", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA=="], - "basic-ftp": ["basic-ftp@5.2.0", "", {}, "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw=="], + "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], diff --git a/package.json b/package.json index c5168d6486..846438a603 100644 --- a/package.json +++ b/package.json @@ -76,5 +76,8 @@ "@anthropic-ai/sdk": "^0.78.0", "xterm": "5", "xterm-addon-fit": "^0.8.0" + }, + "overrides": { + "basic-ftp": "5.3.1" } } From 8b533029830817e766296a880fe197abf0cfc59f Mon Sep 17 00:00:00 2001 From: t Date: Fri, 10 Jul 2026 13:58:07 -0700 Subject: [PATCH 2/2] test(deps): tripwire that no basic-ftp < 5.3.1 resolves in bun.lock Parses package.json overrides + every resolved basic-ftp specifier in bun.lock (including nested paths like get-uri/basic-ftp) and fails if any is below 5.3.1. Deterministic and offline. Fires on the pre-fix tree (basic-ftp@5.2.0) and would also catch a direct-dependency-only bump that leaves a nested vulnerable copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/basic-ftp-security-pin.test.ts | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/basic-ftp-security-pin.test.ts diff --git a/test/basic-ftp-security-pin.test.ts b/test/basic-ftp-security-pin.test.ts new file mode 100644 index 0000000000..a2e9f9f8e3 --- /dev/null +++ b/test/basic-ftp-security-pin.test.ts @@ -0,0 +1,58 @@ +import { describe, test, expect } from 'bun:test'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +const ROOT = join(import.meta.dir, '..'); + +// Security regression guard for the basic-ftp transitive dependency. +// +// basic-ftp reaches the tree only transitively: +// puppeteer-core > @puppeteer/browsers > proxy-agent > pac-proxy-agent > get-uri > basic-ftp +// +// Versions <= 5.3.0 carry four HIGH advisories, all fixed in 5.3.1: +// - GHSA-chqc-8p9q-pq6q CVE-2026-39983 FTP command injection via CRLF (fixed 5.2.1) +// - GHSA-6v7q-wjvx-w8wg incomplete CRLF protection, USER/PASS + MKD bypass (fixed 5.2.2) +// - GHSA-rpmf-866q-6p89 DoS via unbounded multiline control-response buffering +// - GHSA-rp42-5vxx-qpwr DoS via unbounded memory in Client.list() +// +// The fix is a bun `overrides` pin (not a phantom direct dependency): overriding +// forces EVERY basic-ftp in the tree to the safe version, including get-uri's +// nested copy. A direct-dependency bump leaves that nested copy behind — which is +// exactly the failure mode this test is here to catch. See the CVE-2026-39983 +// upgrade fix for the full rationale. +const MIN_SAFE = '5.3.1'; + +function cmpSemver(a: string, b: string): number { + const pa = a.split('.').map(Number); + const pb = b.split('.').map(Number); + for (let i = 0; i < 3; i++) { + if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0); + } + return 0; +} + +describe('basic-ftp security pin (CVE-2026-39983 and siblings)', () => { + test('package.json overrides basic-ftp to a safe version', () => { + const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); + const pin = pkg.overrides?.['basic-ftp']; + expect(pin, 'package.json overrides.basic-ftp must exist').toBeDefined(); + const pinned = String(pin).replace(/^[\^~]/, ''); + expect( + cmpSemver(pinned, MIN_SAFE) >= 0, + `overrides.basic-ftp is "${pin}" but must be >= ${MIN_SAFE}`, + ).toBe(true); + }); + + test('no basic-ftp entry in bun.lock resolves below the safe version', () => { + const lock = readFileSync(join(ROOT, 'bun.lock'), 'utf-8'); + // Match every resolved basic-ftp specifier, including nested paths like + // "get-uri/basic-ftp" that a direct-dependency bump would leave vulnerable. + const versions = [...lock.matchAll(/basic-ftp@(\d+\.\d+\.\d+)/g)].map(m => m[1]); + expect(versions.length, 'expected at least one basic-ftp entry in bun.lock').toBeGreaterThan(0); + const vulnerable = versions.filter(v => cmpSemver(v, MIN_SAFE) < 0); + expect( + vulnerable, + `bun.lock still resolves vulnerable basic-ftp version(s): ${vulnerable.join(', ')}`, + ).toEqual([]); + }); +});