client/web: keep redirects on-site (#10525)

Ensure we don't create Location: header URLs that have leading //, which is a
schema-less reference to arbitrary 3rd-party sites. That is, //example.com/foo
redirects off-site, while /example.com/foo is an on-site path URL.

Fixes tailscale/corp#16268

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
pull/10598/head
Chris Palmer 5 months ago committed by GitHub
parent 727acf96a6
commit b62a3fc895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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")

@ -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()

Loading…
Cancel
Save