diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 82c9d0728..bf0c3c7d7 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -32,6 +32,7 @@ import ( "unicode/utf8" "github.com/kortschak/wol" + "golang.org/x/exp/slices" "golang.org/x/net/dns/dnsmessage" "tailscale.com/client/tailscale/apitype" "tailscale.com/envknob" @@ -542,7 +543,37 @@ func (h *peerAPIHandler) logf(format string, a ...any) { h.ps.b.logf("peerapi: "+format, a...) } +func (h *peerAPIHandler) validateHost(r *http.Request) error { + if r.Host == "peer" { + return nil + } + ap, err := netip.ParseAddrPort(r.Host) + if err != nil { + return err + } + hostIPPfx := netip.PrefixFrom(ap.Addr(), ap.Addr().BitLen()) + if !slices.Contains(h.ps.selfNode.Addresses, hostIPPfx) { + return fmt.Errorf("%v not found in self addresses", hostIPPfx) + } + return nil +} + +func (h *peerAPIHandler) validatePeerAPIRequest(r *http.Request) error { + if r.Referer() != "" { + return errors.New("unexpected Referer") + } + if r.Header.Get("Origin") != "" { + return errors.New("unexpected Origin") + } + return h.validateHost(r) +} + func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := h.validatePeerAPIRequest(r); err != nil { + h.logf("invalid request from %v: %v", h.remoteAddr, err) + http.Error(w, "invalid peerapi request", http.StatusForbidden) + return + } if strings.HasPrefix(r.URL.Path, "/v0/put/") { h.handlePeerPut(w, r) return diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go index 886181f3a..5b59141dc 100644 --- a/ipn/ipnlocal/peerapi_test.go +++ b/ipn/ipnlocal/peerapi_test.go @@ -108,6 +108,7 @@ func hexAll(v string) string { } func TestHandlePeerAPI(t *testing.T) { + const nodeFQDN = "self-node.tail-scale.ts.net." tests := []struct { name string isSelf bool // the peer sending the request is owned by us @@ -402,6 +403,30 @@ func TestHandlePeerAPI(t *testing.T) { bodyContains("bad filename"), ), }, + { + name: "host-val/bad-ip", + isSelf: true, + req: httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil), + checks: checks( + httpStatus(403), + ), + }, + { + name: "host-val/no-port", + isSelf: true, + req: httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil), + checks: checks( + httpStatus(403), + ), + }, + { + name: "host-val/peer", + isSelf: true, + req: httptest.NewRequest("GET", "http://peer/v0/env", nil), + checks: checks( + httpStatus(200), + ), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -417,6 +442,11 @@ func TestHandlePeerAPI(t *testing.T) { }, ps: &peerAPIServer{ b: lb, + selfNode: &tailcfg.Node{ + Addresses: []netip.Prefix{ + netip.MustParsePrefix("100.100.100.101/32"), + }, + }, }, } var rootDir string @@ -425,6 +455,9 @@ func TestHandlePeerAPI(t *testing.T) { e.ph.ps.rootDir = rootDir } e.rr = httptest.NewRecorder() + if tt.req.Host == "example.com" { + tt.req.Host = "100.100.100.101:12345" + } e.ph.ServeHTTP(e.rr, tt.req) for _, f := range tt.checks { f(t, &e) @@ -455,6 +488,9 @@ func TestFileDeleteRace(t *testing.T) { logf: t.Logf, capFileSharing: true, }, + selfNode: &tailcfg.Node{ + Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")}, + }, rootDir: dir, } ph := &peerAPIHandler{ @@ -467,7 +503,7 @@ func TestFileDeleteRace(t *testing.T) { buf := make([]byte, 2<<20) for i := 0; i < 30; i++ { rr := httptest.NewRecorder() - ph.ServeHTTP(rr, httptest.NewRequest("PUT", "/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))]))) + ph.ServeHTTP(rr, httptest.NewRequest("PUT", "http://100.100.100.101:123/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))]))) if res := rr.Result(); res.StatusCode != 200 { t.Fatal(res.Status) }