Skip to content

fix(ssh): follow a jump host's own ProxyJump and name the tunnel before connecting (#2687) - #2691

Merged
datlechin merged 1 commit into
mainfrom
fix/ssh-jump-chain-and-tunnel-disclosure
Sep 9, 2026
Merged

fix(ssh): follow a jump host's own ProxyJump and name the tunnel before connecting (#2687)#2691
datlechin merged 1 commit into
mainfrom
fix/ssh-jump-chain-and-tunnel-disclosure

Conversation

@datlechin

Copy link
Copy Markdown
Member

Follow-up to #2689. Two defects found while investigating #2687, verified before building.

A jump host's own ProxyJump was never followed

buildAuthenticatedChain built the hop list from resolvedPrimary.proxyJump alone, so a bastion that declares a bastion of its own was skipped.

Measured with OpenSSH_10.3p1 on:

Host target
    Hostname 10.0.0.9
    ProxyJump bee
Host bee
    Hostname bee.example.com
    ProxyJump ay
Host ay
    Hostname ay.example.com

ssh -v target issues two proxy commands, -W [bee.example.com]:22 ay then -W [10.0.0.9]:22 bee, so the real chain is ay then bee then target. TablePro stopped at bee and tried to reach it directly, which is either unreachable or the wrong machine.

resolveJumpChain now walks each hop's own ProxyJump first, so the hop nearest the user comes first. It stops at ten levels, which is where ssh stops, so a pair of hosts naming each other terminates instead of exhausting the stack.

The confirmation never named the SSH server

The external-link alert listed host, user and database only. A tunnelled connection's database host is the far end of the tunnel, usually localhost, so the alert asked "connect only if you trust the source of this link" while naming none of the machines the session actually crosses. It now lists the SSH server and every jump host.

The import sheet had the SSH host and auth method but not the SSH username or the jump hosts; both are shown now.

Two related claims that turned out to be wrong

Worth recording, because they look plausible and I reported them before checking:

  • The trust key ignores every SSH field. True, but harmless: SSH config is parsed only from the scheme+ssh:// form (ConnectionURLParser.swift:289), and every other path passes sshHost: nil explicitly. Info.plist registers no +ssh scheme, so those URLs never reach the app. No externally-supplied SSH target can reach the trust store, so the missing fields cannot be exploited.
  • The gate only guards one path. True that ExternalConnectionGate has one production call site, but the four automatic paths that were said to bypass it do not carry externally-supplied connections: reopen-last-session filters to saved ids, Handoff carries only a connection id for a window the user already opened, tunnel recovery rebuilds a tunnel for a session that is already connected, and MCP runs every call through MCPAuthPolicy.resolveAndAuthorize first.

Verification

The jump-chain change has unit coverage for the nested case, a three-level chain, several hops each expanding in place, and a cycle. The alert change is covered by tests that read the built NSAlert's text, including a disabled tunnel that must stay unmentioned. No UI automation: the alert is modal and the import sheet needs a deeplink, neither of which runs deterministically under XCUITest.

https://claude.ai/code/session_01H3kBkbE5KT2gYwy3xFW7UQ

@datlechin
datlechin merged commit 93ae660 into main Sep 9, 2026
8 checks passed
@datlechin
datlechin deleted the fix/ssh-jump-chain-and-tunnel-disclosure branch September 9, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant