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>
This commit is contained in:
parent
727acf96a6
commit
b62a3fc895
@ -15,6 +15,7 @@
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@ -174,6 +175,13 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
|||||||
newAuthURL: opts.NewAuthURL,
|
newAuthURL: opts.NewAuthURL,
|
||||||
waitAuthURL: opts.WaitAuthURL,
|
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 s.mode == ManageServerMode {
|
||||||
if opts.NewAuthURL == nil {
|
if opts.NewAuthURL == nil {
|
||||||
return nil, fmt.Errorf("must provide a NewAuthURL implementation")
|
return nil, fmt.Errorf("must provide a NewAuthURL implementation")
|
||||||
|
@ -939,6 +939,49 @@ func(metricName string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestRequireTailscaleIP(t *testing.T) {
|
||||||
self := &ipnstate.PeerStatus{
|
self := &ipnstate.PeerStatus{
|
||||||
TailscaleIPs: []netip.Addr{
|
TailscaleIPs: []netip.Addr{
|
||||||
@ -1007,7 +1050,7 @@ func TestRequireTailscaleIP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.target, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s.logf = t.Logf
|
s.logf = t.Logf
|
||||||
r := httptest.NewRequest(httpm.GET, tt.target, nil)
|
r := httptest.NewRequest(httpm.GET, tt.target, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
Loading…
Reference in New Issue
Block a user