fix(ssh): allow blank user in VS Code authority (#10072)

This commit is contained in:
Jinjing 2026-07-22 21:10:05 -07:00 committed by GitHub
parent 8448557be9
commit fc05769edf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 43 additions and 9 deletions

View File

@ -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')

View File

@ -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'

View File

@ -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 }
}