mirror of
https://github.com/coder/coder.git
synced 2026-11-02 13:33:30 +08:00
d014d4c372fb7dfdb3b0ea00a9935fec9b9528f9
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4dbb3a236c |
test: fix tailnet connection teardown flake (#27768)
Closes https://github.com/coder/internal/issues/1620 Closes ENG-3043 The callback cleanup added in #20687 stops new node callbacks, but it can still race with one that's already in flight. That callback may call `UpdatePeers` after the destination `tailnet.Conn` closes, so the test fails with `connection closed` even though the redirect behaviour is correct. To fix, we'll just ignore `tailnet.ErrConnClosed` in the asynchronous `stitch` helpers, whilst continuing to assert on every other error. |
||
|
|
eeb2624549 |
fix: pin workspace agent API client to intended agent (#26600)
## Summary The control-plane HTTP client used to talk to workspace agents followed HTTP redirects and trusted the redirected host, letting a malicious workspace agent bounce a coderd request onto a different agent on the shared tailnet. Because the agent HTTP API on port 4 is unauthenticated (it relies on tailnet reachability plus control-plane authorization), this allowed cross-tenant file read/write and remote code execution. This PR refuses redirects and pins every dial to the intended agent. Closes CODAGT-668. ## Problem `agentConn.apiClient` in `codersdk/workspacesdk/agentconn.go` constructed an `http.Client` with no `CheckRedirect`, so Go's default policy followed up to 10 redirects. Its custom `Transport.DialContext` parsed the host from the (post-redirect) request URL and dialed that IP over the shared tailnet, validating only that the port was `AgentHTTPAPIServerPort` (4). It never pinned the connection to the intended `AgentID` / `agentAddress()`. A workspace owner (any regular org member, not just admins) controls their own agent and can make its port-4 handler return a `3xx` `Location` pointing at a victim agent's tailnet IP. When a control-plane action (for example a chat tool or the HTTP MCP server) sends an agent API request to the attacker's agent, coderd acts as a confused deputy and replays the request against the victim: - `301/302/303` rewrite POST to GET, but `307/308` preserve method and body when the body is replayable. The real callers pass replayable bodies, so a redirected `POST /api/v0/write-file` writes attacker-controlled content into the victim workspace and a redirected `POST /api/v0/processes/start` executes it, giving RCE on the victim agent. The dangerous callers run server-side on coderd's single deployment-wide `ServerTailnet`, which is authorized to tunnel to any agent, so the blast radius is cross-tenant / cross-organization (limited in practice to victim agents coderd currently has a live tunnel to). ## Fix In `agentConn.apiClient`: - Set `CheckRedirect: http.ErrUseLastResponse` so the client never follows a redirect. A `3xx` is surfaced to the caller as the response (which the existing `ReadBodyAsError` path turns into an error) instead of being replayed against another host. - Capture the intended agent address once from `AgentID` (`agentAddr := netip.AddrPortFrom(c.agentAddress(), AgentHTTPAPIServerPort)`), reject any dial whose host or port does not match it, and always dial that pinned address rather than the URL-derived host. In `coderd/aitasks.go`, the task app proxy client (`taskAppHTTPClient`) also now sets `CheckRedirect: http.ErrUseLastResponse`. This client dials through `agentConn.DialContext`, which already pins the host to the originating workspace's agent (it takes only the port from the dial address), so it was never cross-agent. The change is hardening for parity so a malicious app cannot bounce the request to a different port on the same agent. ## Hardening and defense in depth The two layers are independent. `CheckRedirect` removes the redirect-following behavior entirely, and the dial pinning guarantees that even a request constructed with a foreign host can only ever reach the intended agent. Removing either one in the future cannot, on its own, reintroduce the cross-agent vector. ## Tests - `codersdk/workspacesdk/agentconn_redirect_test.go` builds a three-peer tailnet (client, attacker, victim). The attacker agent redirects to the victim's port-4 URL, and the test asserts that `GET` `302`, `POST` `307`, and `POST` `308` all return an error and that the victim is never contacted. - `coderd/aitasks_internal_test.go` adds `TestTaskAppHTTPClient_RejectsRedirect`, which verifies the task app client surfaces a `307` instead of following it to a stand-in victim. ## Why this closes the whole vulnerability class `apiClient` is the only HTTP chokepoint to the agent port-4 API, so fixing it covers every server-side caller: - Every agent HTTP API method in `agentConn` funnels through `apiClient`, either via `apiRequest`, a direct `apiClient(ctx).Do(...)` (`ExecuteDesktopAction`), or as the websocket `HTTPClient` (`WatchContainers`, `WatchGit`, `ConnectDesktopVNC`). The websocket handshake matters here: `coder/websocket` follows `3xx` during the handshake by default and only requires `101` on the final hop, but it honors the underlying client's `CheckRedirect`, so reusing `apiClient` closes the websocket paths too. - The HTTP MCP server coderd hosts at `/api/experimental/mcp/http` registers tools (`coder_workspace_bash`, `_write_file`, `_read_file`, `_edit_files`, etc.) that reach the agent through `workspacesdk.AgentConn` methods, so they go through `apiClient` and are covered. The same is true for agent-hosted MCP, which coderd reaches only via `agentConn.CallMCPTool` / `ListMCPTools`. coderd never opens an MCP client connection directly to an agent over the tailnet. - Raw-TCP agent services (reconnecting PTY, SSH, speedtest, generic `DialContext`) speak non-HTTP protocols and have no redirect surface. The workspace apps reverse proxy targets user app ports, not port 4, forwards `3xx` to the browser rather than following them, and pins its transport to the request's agent. - `provisionerd` does not talk to the agent HTTP API at all. No other server-side client follows redirects to an agent-controllable tailnet host, so no further redirect changes are required for this class. |
||
|
|
35f31d9820 |
fix(codersdk/workspacesdk): fix leaking AwaitReachable when agent unreachable (#26342)
Noticed when enabling the goleak checker in chatd:
```
=== FAIL: coderd/x/chatd (0.00s)
PASS
goleak: Errors on successful test run: found unexpected goroutines:
[Goroutine 108179 in state select, with github.com/coder/coder/v2/tailnet.(*Conn).AwaitReachable on top of the stack:
github.com/coder/coder/v2/tailnet.(*Conn).AwaitReachable(0x2c35e171d760, {0x74b37a8?, 0x2c35f6886330?}, {{0x0?, 0x0?}, {0x2c35def838c0?}})
/home/runner/work/coder/coder/tailnet/conn.go:647 +0x2ae
github.com/coder/coder/v2/codersdk/workspacesdk.(*agentConn).AwaitReachable(0x2c35e4f18440, {0x74b37e0?, 0x2c35f45680a0?})
/home/runner/work/coder/coder/codersdk/workspacesdk/agentconn.go:172 +0x12b
github.com/coder/coder/v2/codersdk/workspacesdk.(*agentConn).apiRequest.(*agentConn).apiClient.func1({0x74b37e0, 0x2c35f45680a0}, {0x61a8946?, 0x60fa2a0?}, {0x2c35e0af7380, 0x2b})
/home/runner/work/coder/coder/codersdk/workspacesdk/agentconn.go:1381 +0x212
net/http.(*Transport).dial(0x2c35e0af6210?, {0x74b37e0?, 0x2c35f45680a0?}, {0x61a8946?, 0xa0e255?}, {0x2c35e0af7380?, 0xa1456f?})
/home/runner/work/_temp/mise-data/installs/go/1.26.4/src/net/http/transport.go:1307 +0xd2
net/http.(*Transport).dialConn(0x2c35f8d8b380, {0x74b37e0, 0x2c35f45680a0}, {{}, 0x0, {0x2c35fc5a3e50, 0x4}, {0x2c35e0af7380, 0x2b}, 0x0}, ...)
/home/runner/work/_temp/mise-data/installs/go/1.26.4/src/net/http/transport.go:1815 +0x847
net/http.(*Transport).dialConnFor(0x2c35f8d8b380, 0x2c35e287a580)
/home/runner/work/_temp/mise-data/installs/go/1.26.4/src/net/http/transport.go:1648 +0xd2
net/http.(*Transport).startDialConnForLocked.func1()
/home/runner/work/_temp/mise-data/installs/go/1.26.4/src/net/http/transport.go:1629 +0x35
created by net/http.(*Transport).startDialConnForLocked in goroutine 107872
/home/runner/work/_temp/mise-data/installs/go/1.26.4/src/net/http/transport.go:1628 +0x112
Goroutine 108180 in state select, with github.com/cenkalti/backoff/v4.(*Ticker).run on top of the stack:
github.com/cenkalti/backoff/v4.(*Ticker).run(0x2c35f8517860)
/home/runner/go/pkg/mod/github.com/cenkalti/backoff/v4@v4.3.0/ticker.go:70 +0x13f
created by github.com/cenkalti/backoff/v4.NewTickerWithTimer in goroutine 108179
/home/runner/go/pkg/mod/github.com/cenkalti/backoff/v4@v4.3.0/ticker.go:49 +0x16c
]
FAIL github.com/coder/coder/v2/coderd/x/chatd 126.492s
```
Closes https://github.com/coder/internal/issues/1595
|
||
|
|
c650aabbef |
chore: standardize on *_internal_test.go for white-box tests (#25601)
My agent added `//nolint:testpackage` to a test file on one of my PRs. Again. This PR cleans it up across the entire repo and updates the in-repo conventions so future agents stop doing it. The repo already has a precedent for white-box tests that need to touch unexported symbols: `*_internal_test.go` (145+ existing files). The `testpackage` linter's default `skip-regexp` exempts that filename suffix, so the `//nolint:testpackage` directive is unnecessary in every case where someone reached for it. This PR renames 51 such files to `*_internal_test.go` via `git mv` so blame and history follow, and strips the dead directive from 2 files that were already correctly named (`coderd/oauth2provider/authorize_internal_test.go`, `coderd/x/chatd/advisor_internal_test.go`). `.claude/docs/TESTING.md` now documents the rule explicitly under *Test Package Naming*, which is imported into the root `AGENTS.md` via `@.claude/docs/TESTING.md`. The rule: prefer `package foo_test`; if you need internal access, rename the file to `*_internal_test.go` rather than adding a nolint directive. |
||
|
|
1cf0354f72 |
feat: add plan mode with restricted tool boundary (#24236)
> This PR was authored by Mux on behalf of Mike. ## Summary - add persistent plan mode for chats and the chat-specific plan file flow - add structured planning tools such as `ask_user_question` and `propose_plan` - keep `write_file` and `edit_files` constrained to the chat-specific plan file during plan turns - allow shell exploration in plan mode, including subagents, via `execute` and `process_output` - block implementation-oriented, provider-native, MCP, dynamic, and computer-use tools during plan turns - update the chat UI, tests, and docs for the new planning flow |