Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@
"@anthropic-ai/sdk": "^0.78.0",
"xterm": "5",
"xterm-addon-fit": "^0.8.0"
},
"overrides": {
"basic-ftp": "5.3.1"
}
}
58 changes: 58 additions & 0 deletions test/basic-ftp-security-pin.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});