From 6d39e4948048bef2d8752acf87ef221118998135 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Fri, 24 Jul 2026 03:24:53 -0400 Subject: [PATCH] fix(ssh): accept GitHub restricted-shell SSH probes (#6988) (#7659) * fix(ssh): accept GitHub restricted-shell SSH probes (#6988) * fix: match first stderr line for GitHub restricted-shell probe (bug-bash takeover) Co-authored-by: Orca --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Orca --- src/main/ssh/ssh-connection.test.ts | 222 ++++++++++++++++++++++++++++ src/main/ssh/ssh-connection.ts | 52 ++++++- 2 files changed, 267 insertions(+), 7 deletions(-) diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index 750932289..1ab923bee 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -1099,6 +1099,228 @@ describe('SshConnection', () => { expect(conn.canRunConcurrentExecCommands()).toBe(false) }) + it('accepts GitHub restricted-shell SSH probes with resolved user fallback', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'github.com', user: 'git' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: undefined + }), + createCallbacks() + ) + + await conn.connect() + + expect(conn.getState().status).toBe('connected') + expect(conn.usesSystemSshTransport()).toBe(true) + }) + + it('accepts GitHub restricted-shell SSH probes with resolved host and target username', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'github.com', user: undefined }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'git' + }), + createCallbacks() + ) + + await conn.connect() + + expect(conn.getState().status).toBe('connected') + expect(conn.usesSystemSshTransport()).toBe(true) + }) + + it('accepts ssh.github.com restricted-shell SSH probes', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'ssh.github.com', user: 'git' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'ssh.github.com', + host: 'ssh.github.com', + username: 'git' + }), + createCallbacks() + ) + + await conn.connect() + + expect(conn.getState().status).toBe('connected') + expect(conn.usesSystemSshTransport()).toBe(true) + }) + + it('accepts GitHub restricted-shell SSH probes with the real git:// advisory transcript', async () => { + // Real 4-line stderr GitHub returns for an invalid command (issue #6988). + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'github.com', user: 'git' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel( + 1, + 'Invalid command: echo ORCA-SYSTEM-SSH-OK\n' + + ' You appear to be using ssh to clone a git:// URL.\n' + + ' Make sure your core.gitProxy config option and the\n' + + ' GIT_PROXY_COMMAND environment variable are NOT set.' + ) + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'git' + }), + createCallbacks() + ) + + await conn.connect() + + expect(conn.getState().status).toBe('connected') + expect(conn.usesSystemSshTransport()).toBe(true) + }) + + it('accepts GitHub restricted-shell SSH probes when OpenSSH config resolution fails', async () => { + vi.stubEnv('ORCA_SSH_FORCE_SYSTEM_TRANSPORT', '1') + vi.mocked(resolveWithSshG).mockRejectedValueOnce(new Error('ssh -G failed')) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'git' + }), + createCallbacks() + ) + + await conn.connect() + + expect(conn.getState().status).toBe('connected') + expect(conn.usesSystemSshTransport()).toBe(true) + expect(conn.getSystemSshResolvedConfig()).toBeNull() + }) + + it('rejects non-GitHub SSH probes with GitHub invalid-command text', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'gitlab.com', user: 'git' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'git' + }), + createCallbacks() + ) + + await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)') + expect(conn.usesSystemSshTransport()).toBe(false) + }) + + it('accepts GitHub restricted-shell SSH probes when target username overrides resolved user', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'github.com', user: 'deploy' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'git' + }), + createCallbacks() + ) + + await conn.connect() + + expect(conn.getState().status).toBe('connected') + expect(conn.usesSystemSshTransport()).toBe(true) + }) + + it('rejects GitHub restricted-shell SSH probes when target username overrides resolved git user', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'github.com', user: 'git' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'deploy' + }), + createCallbacks() + ) + + await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)') + expect(conn.usesSystemSshTransport()).toBe(false) + }) + + it('rejects GitHub restricted-shell SSH probes with extra stderr text', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'github.com', user: 'git' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel( + 1, + 'remote: rejected\nInvalid command: echo ORCA-SYSTEM-SSH-OK\ntry again' + ) + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'git' + }), + createCallbacks() + ) + + await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)') + expect(conn.usesSystemSshTransport()).toBe(false) + }) + + it('rejects GitHub restricted-shell SSH probes for non-git users', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce( + createResolvedConfig({ hostname: 'github.com', user: 'deploy' }) + ) + spawnSystemSshCommandMock.mockImplementation(() => + createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK') + ) + const conn = new SshConnection( + createTarget({ + configHost: 'github.com', + host: 'github.com', + username: 'deploy' + }), + createCallbacks() + ) + + await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)') + expect(conn.usesSystemSshTransport()).toBe(false) + }) + it('retries a failed system SSH probe without ControlMaster and disables mux for the session', async () => { getOrcaControlSocketPathMock.mockImplementation( (_target: SshTarget, options?: { disableControlMaster?: boolean }) => diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index baba633d7..b16cbce86 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -66,6 +66,36 @@ function cloneResolvedConfig(config: SshResolvedConfig | null): SshResolvedConfi return { ...config, identityFile: [...config.identityFile] } } +function isGitHubRestrictedShellProbeSuccess( + target: SshTarget, + resolvedConfig: SshResolvedConfig | null, + code: number | null, + stderr: string +): boolean { + if (code !== 1) { + return false + } + + const effectiveUser = (target.username?.trim() || resolvedConfig?.user?.trim())?.toLowerCase() + if (effectiveUser !== 'git') { + return false + } + + // GitHub appends git:// advisory lines after the invalid-command line (issue #6988), so match the first line only. + const firstLine = stderr.split('\n', 1)[0]?.trim() + if (firstLine !== 'Invalid command: echo ORCA-SYSTEM-SSH-OK') { + return false + } + + const resolvedHost = resolvedConfig?.hostname?.trim() + const hostCandidates = resolvedHost ? [resolvedHost] : [target.host, target.configHost] + + return hostCandidates.some((host) => { + const normalizedHost = host?.trim().toLowerCase() + return normalizedHost === 'github.com' || normalizedHost === 'ssh.github.com' + }) +} + export class SshConnection { private client: SshClient | null = null private proxyProcess: ChildProcess | null = null @@ -847,16 +877,24 @@ export class SshConnection { reject(new Error('SSH connection attempt was cancelled')) return } - if (code !== 0 || !stdout.includes('ORCA-SYSTEM-SSH-OK')) { - reject( - new Error( - `System SSH probe failed${code != null ? ` (exit ${code})` : ''}.${stderr ? ` stderr: ${stderr.trim()}` : ''}` - ) + if ( + (code === 0 && stdout.includes('ORCA-SYSTEM-SSH-OK')) || + isGitHubRestrictedShellProbeSuccess( + this.target, + this.systemSshResolvedConfig, + code, + stderr ) + ) { + this.setState('connected') + resolve() return } - this.setState('connected') - resolve() + reject( + new Error( + `System SSH probe failed${code != null ? ` (exit ${code})` : ''}.${stderr ? ` stderr: ${stderr.trim()}` : ''}` + ) + ) }) } const timeout = setTimeout(() => {