From b9a41be56c7a863faec85aa47fac375d82b96b9d Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:10:07 -0700 Subject: [PATCH] fix(ssh): allow blank user in VS Code authority --- src/main/ipc/shell.test.ts | 23 +++++++++++++++++++++++ src/main/ssh/vscode-ssh-authority.test.ts | 15 +++++++++++++-- src/main/ssh/vscode-ssh-authority.ts | 14 +++++++------- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/main/ipc/shell.test.ts b/src/main/ipc/shell.test.ts index 2213c924af4..7193da4beaa 100644 --- a/src/main/ipc/shell.test.ts +++ b/src/main/ipc/shell.test.ts @@ -542,6 +542,29 @@ describe('registerShellHandlers', () => { ]) }) + it('opens a manual port-22 target with a host-only authority when username is blank', async () => { + sshTargets.set( + 'ssh-1', + createSshTarget({ + source: 'manual', + configHost: 'builder.example.com', + host: 'builder.example.com', + username: '' + }) + ) + resolveCliCommandMock.mockReturnValueOnce('/usr/local/bin/code') + const handler = getHandler('shell:openInExternalEditor') + + await expect( + handler({}, { path: '/srv/project', command: 'code', connectionId: 'ssh-1' }) + ).resolves.toEqual({ ok: true }) + expect(getSpawnArgsForWindowsMock).toHaveBeenCalledWith('/usr/local/bin/code', [ + '--remote', + 'ssh-remote+builder.example.com', + '/srv/project' + ]) + }) + it('rejects relative SSH paths before resolving or spawning a launcher', async () => { sshTargets.set('ssh-1', createSshTarget()) const handler = getHandler('shell:openInExternalEditor') diff --git a/src/main/ssh/vscode-ssh-authority.test.ts b/src/main/ssh/vscode-ssh-authority.test.ts index 862842b6ede..2816c1bc4bf 100644 --- a/src/main/ssh/vscode-ssh-authority.test.ts +++ b/src/main/ssh/vscode-ssh-authority.test.ts @@ -34,6 +34,16 @@ describe('resolveVsCodeSshAuthority', () => { ).toEqual({ ok: true, authority: 'ada@builder.example.com' }) }) + it.each(['', ' '])( + 'uses a host-only authority for a manual port-22 target without a username', + (username) => { + expect(resolveVsCodeSshAuthority(createTarget({ username }))).toEqual({ + ok: true, + authority: 'builder.example.com' + }) + } + ) + it('requires an alias for manual targets on non-default ports', () => { expect( resolveVsCodeSshAuthority( @@ -49,11 +59,12 @@ describe('resolveVsCodeSshAuthority', () => { it.each([ createTarget({ host: ' ' }), - createTarget({ username: '' }), createTarget({ host: 'builder\nmalicious' }), + createTarget({ username: '\n' }), + createTarget({ username: 'ada\nmalicious' }), createTarget({ source: 'ssh-config', configHost: '\u0000builder' }), createTarget({ port: 0 }) - ])('rejects incomplete or unsafe target fields', (target) => { + ])('rejects invalid or unsafe target fields', (target) => { expect(resolveVsCodeSshAuthority(target)).toEqual({ ok: false, reason: 'ssh-target-invalid' diff --git a/src/main/ssh/vscode-ssh-authority.ts b/src/main/ssh/vscode-ssh-authority.ts index d29bac64768..055baac65d4 100644 --- a/src/main/ssh/vscode-ssh-authority.ts +++ b/src/main/ssh/vscode-ssh-authority.ts @@ -6,9 +6,9 @@ export type VsCodeSshAuthorityResult = | { ok: false; reason: 'ssh-target-invalid' } | { ok: false; reason: 'ssh-alias-required'; host: string; port: number } -function isValidAuthorityPart(value: string): boolean { +function isValidAuthorityPart(value: string, allowEmpty = false): boolean { return ( - value.length > 0 && + (allowEmpty || value.trim().length > 0) && !Array.from(value).some((character) => { const codePoint = character.codePointAt(0) ?? 0 return codePoint <= 0x1f || codePoint === 0x7f @@ -18,17 +18,17 @@ function isValidAuthorityPart(value: string): boolean { export function resolveVsCodeSshAuthority(target: SshTarget): VsCodeSshAuthorityResult { if (isOpenSshConfigBackedTarget(target)) { - const configHost = target.configHost?.trim() ?? '' + const configHost = target.configHost ?? '' return isValidAuthorityPart(configHost) - ? { ok: true, authority: configHost } + ? { ok: true, authority: configHost.trim() } : { ok: false, reason: 'ssh-target-invalid' } } const host = target.host.trim() const username = target.username.trim() if ( - !isValidAuthorityPart(host) || - !isValidAuthorityPart(username) || + !isValidAuthorityPart(target.host) || + !isValidAuthorityPart(target.username, true) || !Number.isInteger(target.port) || target.port < 1 || target.port > 65_535 @@ -38,5 +38,5 @@ export function resolveVsCodeSshAuthority(target: SshTarget): VsCodeSshAuthority if (target.port !== 22) { return { ok: false, reason: 'ssh-alias-required', host, port: target.port } } - return { ok: true, authority: `${username}@${host}` } + return { ok: true, authority: username ? `${username}@${host}` : host } }