diff --git a/client/web/web.go b/client/web/web.go index 1fee3c62d..a3310bbfe 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -15,6 +15,7 @@ import ( "net/http" "net/netip" "os" + "path" "path/filepath" "slices" "strings" @@ -174,6 +175,13 @@ func NewServer(opts ServerOpts) (s *Server, err error) { newAuthURL: opts.NewAuthURL, waitAuthURL: opts.WaitAuthURL, } + if opts.PathPrefix != "" { + // In enforcePrefix, we add the necessary leading '/'. If we did not + // strip 1 or more leading '/'s here, we would end up redirecting + // clients to e.g. //example.com (a schema-less URL that points to + // another site). See https://github.com/tailscale/corp/issues/16268. + s.pathPrefix = strings.TrimLeft(path.Clean(opts.PathPrefix), "/\\") + } if s.mode == ManageServerMode { if opts.NewAuthURL == nil { return nil, fmt.Errorf("must provide a NewAuthURL implementation") diff --git a/client/web/web_test.go b/client/web/web_test.go index bbf764bc7..ee46551fe 100644 --- a/client/web/web_test.go +++ b/client/web/web_test.go @@ -939,6 +939,49 @@ func TestServeAPIAuthMetricLogging(t *testing.T) { } } +func TestNoOffSiteRedirect(t *testing.T) { + options := ServerOpts{ + Mode: LoginServerMode, + // Emulate the admin using a --prefix option with leading slashes: + PathPrefix: "//evil.example.com/goat", + CGIMode: true, + } + s, err := NewServer(options) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + target string + wantHandled bool + wantLocation string + }{ + { + name: "2-slashes", + target: "http://localhost//evil.example.com/goat", + // We must also get the trailing slash added: + wantLocation: "/evil.example.com/goat/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s.logf = t.Logf + r := httptest.NewRequest(httpm.GET, tt.target, nil) + w := httptest.NewRecorder() + s.ServeHTTP(w, r) + res := w.Result() + defer res.Body.Close() + + location := w.Header().Get("Location") + if location != tt.wantLocation { + t.Errorf("request(%q) got wrong location; want=%q, got=%q", tt.target, tt.wantLocation, location) + } + }) + } +} + func TestRequireTailscaleIP(t *testing.T) { self := &ipnstate.PeerStatus{ TailscaleIPs: []netip.Addr{ @@ -1007,7 +1050,7 @@ func TestRequireTailscaleIP(t *testing.T) { } for _, tt := range tests { - t.Run(tt.target, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { s.logf = t.Logf r := httptest.NewRequest(httpm.GET, tt.target, nil) w := httptest.NewRecorder()