Repository navigation
cmux ssh: require a per-tunnel credential on the browser proxy - #15132
austinywang wants to merge 6 commits into
Conversation
The loopback SOCKS5 and HTTP CONNECT listener that serves the ssh workspace browser accepts any local client. Add a per-tunnel BrowserProxyCredential and an optional session seam, then test that a handshake without it is refused before a daemon stream opens and that the credentialed handshake still relays bytes. The Network.framework cases drive URLSession through ProxyConfiguration, the stack WKWebView uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loopback SOCKS5 and HTTP CONNECT listener for the ssh workspace browser now requires the credential its tunnel minted. SOCKS5 only accepts username/password authentication (RFC 1929), CONNECT needs a matching Proxy-Authorization: Basic header, and both compare in constant time before any daemon stream opens. A refused client gets 05 FF, 01 01 or 407 and the connection closes. The broker mints a fresh credential per tunnel start and hands it to the tunnel and the BrowserProxyEndpoint. The browser applies it to its WKWebsiteDataStore SOCKS5 and CONNECT proxy configurations, and the favicon session moves from the legacy proxy dictionary, which cannot carry a credential, to a credentialed ProxyConfiguration. Descriptions of the credential and endpoint are redacted, and workspace.remote.status keeps reporting the endpoint without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
All contributors have signed the CLA ✍️ ✅ |
|
Warning Review limit reachedNext included review available in 2 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (28)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
CI failure attributionCI passes on Written by |
A proxy 407 on a URLSession without a delegate reaches the system, which asks the user for a proxy password. Route the proxy test's session through a delegate and test that it cancels proxy challenges while leaving server challenges to the default handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The remote proxy credential rotates on each tunnel start, and another process can take a freed loopback port. A stale or wrong credential then reaches the system, which asks the user for a proxy password and would send what they type to that port. Cancel proxy challenges on the favicon session and in the web view and popup delegates of remote and cloud panes, whose only proxies are cmux proxies that already carry their credential. Local panes keep the default handling for their system proxy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProxyConfiguration is a struct over a reference, and applyCredential is non-mutating, so the var bindings added with the per-tunnel credential drew two "never mutated" warnings in BrowserPanel.swift. The macOS compile admission gate refuses new Swift warnings over the budget, which declined the PR. The test helper had the same binding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dogfood tours of
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing as superseded by merged PR #15116, which consolidated this SSH/security/local-state hardening into main. |





Summary
The ssh workspace browser reaches remote services through a loopback SOCKS5 and HTTP CONNECT listener. Each accepted handshake opens a daemon
proxy.openstream on the remote host. Until now the listener accepted any local client.Each tunnel start now mints a random
BrowserProxyCredential: usernamecmuxand a 64-hex-digit password. The listener checks it on every handshake, with a constant-time compare, before any daemon stream opens:05 FF, and a wrong credential gets01 01. Either way the connection closes.Proxy-Authorization: Basic …. Without it, the listener answers407withProxy-Authenticate: Basic realm="cmux"and closes.The browser applies the credential to both
ProxyConfigurations on itsWKWebsiteDataStore. The faviconURLSessionswitches fromconnectionProxyDictionaryto the sameproxyConfigurations, so it carries the credential too.A refused credential never turns into a password prompt. Without a delegate, a proxy
407reaches the system, which asks the user for a proxy password and would send the answer to whatever holds the loopback port. That can happen after a tunnel restart rotates the credential, or when another process takes a freed port. The newManagedProxySessionDelegatecancels proxy challenges instead. It is used on the favicon session, and the same check runs in the web view and popup navigation delegates of remote and cloud panes. Local panes keep the default handling for their system proxy.The credential stays out of logs, argv and
workspace.remote.status.BrowserProxyCredentialandBrowserProxyEndpointprint as redacted descriptions. The status payload still reports host, port and asocks5://URL, without the credential.Testing
Regression test, run the same way at each commit:
05 00or200 Connection Established, and daemon streams opened.Proxy prompt follow-up, run the same way at each commit:
.performDefaultHandlinginstead of.cancelAuthenticationChallenge.On f39305d,
RemoteDaemonProxySessionAuthenticationTestspassed 8 tests in 0.276 seconds. ItsURLSessionnow goes through the delegate, so the no-credential CONNECT case is refused without opening the system proxy-password dialog.verify-local.py --affected origin/main --swift-changed origin/mainpassed 15/15 checks.The suite covers raw SOCKS5 and CONNECT handshakes with a missing, wrong or correct credential. It also drives
URLSessionthrough Network.frameworkProxyConfiguration, the stackWKWebViewuses, with and without the credential.Full package runs on 645d033 all passed:
CmuxCore adds
BrowserProxyCredentialTests, which checks:matchesneeds the exact bytes.python3 scripts/verify-local.pywith automatic selection passed 5/5 checks: swift-syntax, wire-app-sources, test-wiring, package-groups and feature-flags.Not run:
Sources/Panels/BrowserPanel.swift,BrowserNavigationDelegate.swift,BrowserPopupWindowController.swiftand the threeGhosttyConfigTestscall sites. No app test drives a web view proxy challenge.tests_v2:test_ssh_remote_docker_forwarding.py,test_ssh_remote_docker_reconnect.pyandtest_ssh_remote_proxy_bind_conflict.pysend unauthenticated SOCKS5 and CONNECT to the endpoint fromworkspace.remote.status. The listener now refuses those requests, and the status payload deliberately omits the credential. These scripts are not in CI and were not updated here.No user-facing strings changed, so there is no localization impact.
Changelog
Fixed: The ssh workspace browser proxy now requires a per-tunnel credential, so other local accounts can't route connections through the remote host
Checklist
docs/remote-daemon-spec.md,daemon/remote/README.md)🤖 Generated with Claude Code