Files
coder/coderd/workspaceapps/proxy_test.go
T
George K b5ef700dd6 fix!: only trust x-forwarded-host from configured trusted proxies (#26204)
Subdomain app routing derived the app identity from
httpapi.RequestHost, which returned the client-supplied
X-Forwarded-Host header verbatim. No middleware validated or stripped
that header, so a request from an untrusted peer could forge it. Since
the application_connect cookie is scoped to the wildcard apps domain,
JavaScript in a share=authenticated app could fetch() with a forged
X-Forwarded-Host pointing at a victim's owner-only app; coderd routed
and authorized the request as the victim and returned the private app
response same-origin to the attacker.

Replace RequestHost with httpmw.EffectiveHost, which honors
X-Forwarded-Host only when the original socket peer is a configured
trusted origin, otherwise falling back to the received Host header.
This ties host trust to the same RealIPConfig model already used for
X-Forwarded-For and -Proto. Wire it into HandleSubdomain for both
coderd and wsproxy, and log both the effective host and the raw
received_host.

Add coverage: EffectiveHost unit tests assert the trust decision uses
the socket peer rather than the spoofable forwarded client IP, and a
HandleSubdomain test confirms a forged X-Forwarded-Host from an
untrusted peer never reaches token resolution.

Refs: https://linear.app/codercom/issue/PLAT-259
2026-06-11 10:55:00 -07:00

91 lines
2.6 KiB
Go

package workspaceapps_test
// App tests can be found in the apptest package.
import (
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
"github.com/coder/coder/v2/testutil"
)
type fakeSignedTokenProvider struct {
fromRequestCalls int
issueCalls int
}
func (s *fakeSignedTokenProvider) FromRequest(_ *http.Request) (*workspaceapps.SignedToken, bool) {
s.fromRequestCalls++
return nil, false
}
func (s *fakeSignedTokenProvider) Issue(_ context.Context, _ http.ResponseWriter, _ *http.Request, _ workspaceapps.IssueTokenRequest) (*workspaceapps.SignedToken, string, bool) {
s.issueCalls++
return nil, "", false
}
func TestHandleSubdomain_IgnoresUntrustedForwardedHost(t *testing.T) {
t.Parallel()
hostnamePattern := "*--apps.test.coder.com"
hostnameRegex, err := appurl.CompileHostnamePattern(hostnamePattern)
require.NoError(t, err)
dashboardURL, err := url.Parse("https://dashboard.test.coder.com")
require.NoError(t, err)
provider := &fakeSignedTokenProvider{}
srv := workspaceapps.NewServer(workspaceapps.ServerOptions{
Logger: testutil.Logger(t),
DashboardURL: dashboardURL,
AccessURL: dashboardURL,
Hostname: hostnamePattern,
HostnameRegex: hostnameRegex,
RealIPConfig: &httpmw.RealIPConfig{
TrustedOrigins: []*net.IPNet{{
IP: net.ParseIP("10.0.0.1"),
Mask: net.CIDRMask(32, 32),
}},
},
SignedTokenProvider: provider,
})
forgedHost := appurl.ApplicationURL{
AppSlugOrPort: "app",
WorkspaceName: "workspace",
Username: "victim",
}.String() + "--apps.test.coder.com"
nextCalled := false
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
nextCalled = true
})
// Given: a request with a forged X-Forwarded-Host set to a valid
// app hostname, and an immediate peer outside the trusted proxy
// config.
req := httptest.NewRequest(http.MethodGet, "https://dashboard.test.coder.com/", nil)
req.Header.Set(httpapi.XForwardedHostHeader, forgedHost)
req.RemoteAddr = "17.18.19.20:1234"
// When: HandleSubdomain runs.
srv.HandleSubdomain()(next).ServeHTTP(httptest.NewRecorder(), req)
// Then: it ignores untrusted X-Forwarded-Host, so the received
// dashboard host is used, the request falls through to the next
// handler, and the signed app token provider is never called.
require.True(t, nextCalled)
require.Zero(t, provider.fromRequestCalls)
require.Zero(t, provider.issueCalls)
}