mirror of
https://github.com/coder/coder.git
synced 2026-09-22 21:22:17 +08:00
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
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package agentchat_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/agent/agentchat"
|
|
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
|
|
"github.com/coder/coder/v2/coderd/tracing"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestMiddlewareAccessLog(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
chatID := uuid.New()
|
|
ancestorID := uuid.New()
|
|
sink := testutil.NewFakeSink(t)
|
|
handler := tracing.StatusWriterMiddleware(loggermw.Logger(sink.Logger(), nil)(
|
|
agentchat.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
})),
|
|
))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set(workspacesdk.CoderChatIDHeader, chatID.String())
|
|
req.Header.Set(workspacesdk.CoderAncestorChatIDsHeader, mustMarshalJSON(t, []string{ancestorID.String()}))
|
|
rw := httptest.NewRecorder()
|
|
handler.ServeHTTP(rw, req)
|
|
require.Equal(t, http.StatusNoContent, rw.Code)
|
|
|
|
entries := sink.Entries()
|
|
require.Len(t, entries, 1)
|
|
fields := fieldsByName(entries[0].Fields)
|
|
require.Equal(t, chatID.String(), fields["chat_id"])
|
|
require.Equal(t, []string{ancestorID.String()}, fields["ancestor_chat_ids"])
|
|
}
|
|
|
|
func TestMiddlewareWithoutChatHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sink := testutil.NewFakeSink(t)
|
|
handler := tracing.StatusWriterMiddleware(loggermw.Logger(sink.Logger(), nil)(
|
|
agentchat.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
})),
|
|
))
|
|
|
|
rw := httptest.NewRecorder()
|
|
handler.ServeHTTP(rw, httptest.NewRequest(http.MethodGet, "/test", nil))
|
|
require.Equal(t, http.StatusNoContent, rw.Code)
|
|
|
|
entries := sink.Entries()
|
|
require.Len(t, entries, 1)
|
|
fields := fieldsByName(entries[0].Fields)
|
|
require.NotContains(t, fields, "chat_id")
|
|
require.NotContains(t, fields, "ancestor_chat_ids")
|
|
}
|
|
|
|
func TestMiddlewareContextFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
chatID := uuid.New()
|
|
sink := testutil.NewFakeSink(t)
|
|
handler := tracing.StatusWriterMiddleware(loggermw.Logger(sink.Logger(), nil)(
|
|
agentchat.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
sink.Logger().With(agentchat.Fields(r.Context())...).Info(r.Context(), "handler log")
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
})),
|
|
))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set(workspacesdk.CoderChatIDHeader, chatID.String())
|
|
rw := httptest.NewRecorder()
|
|
handler.ServeHTTP(rw, req)
|
|
require.Equal(t, http.StatusNoContent, rw.Code)
|
|
|
|
entries := sink.Entries()
|
|
require.Len(t, entries, 2)
|
|
for _, entry := range entries {
|
|
if entry.Message != "handler log" {
|
|
continue
|
|
}
|
|
fields := fieldsByName(entry.Fields)
|
|
require.Equal(t, chatID.String(), fields["chat_id"])
|
|
return
|
|
}
|
|
t.Fatal("handler log entry not found")
|
|
}
|
|
|
|
func fieldsByName(fields []slog.Field) map[string]any {
|
|
byName := make(map[string]any, len(fields))
|
|
for _, field := range fields {
|
|
byName[field.Name] = field.Value
|
|
}
|
|
return byName
|
|
}
|