ipn/ipnlocal: add some validation to PeerAPI

Updates tailscale/corp#7948

Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali 2022-11-16 21:53:51 +05:00 committed by Maisem Ali
parent 1f4669a380
commit 1e78fc462c
2 changed files with 68 additions and 1 deletions

View File

@ -32,6 +32,7 @@
"unicode/utf8" "unicode/utf8"
"github.com/kortschak/wol" "github.com/kortschak/wol"
"golang.org/x/exp/slices"
"golang.org/x/net/dns/dnsmessage" "golang.org/x/net/dns/dnsmessage"
"tailscale.com/client/tailscale/apitype" "tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob" "tailscale.com/envknob"
@ -542,7 +543,37 @@ func (h *peerAPIHandler) logf(format string, a ...any) {
h.ps.b.logf("peerapi: "+format, a...) 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) { 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/") { if strings.HasPrefix(r.URL.Path, "/v0/put/") {
h.handlePeerPut(w, r) h.handlePeerPut(w, r)
return return

View File

@ -108,6 +108,7 @@ func hexAll(v string) string {
} }
func TestHandlePeerAPI(t *testing.T) { func TestHandlePeerAPI(t *testing.T) {
const nodeFQDN = "self-node.tail-scale.ts.net."
tests := []struct { tests := []struct {
name string name string
isSelf bool // the peer sending the request is owned by us isSelf bool // the peer sending the request is owned by us
@ -402,6 +403,30 @@ func TestHandlePeerAPI(t *testing.T) {
bodyContains("bad filename"), 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -417,6 +442,11 @@ func TestHandlePeerAPI(t *testing.T) {
}, },
ps: &peerAPIServer{ ps: &peerAPIServer{
b: lb, b: lb,
selfNode: &tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.100.100.101/32"),
},
},
}, },
} }
var rootDir string var rootDir string
@ -425,6 +455,9 @@ func TestHandlePeerAPI(t *testing.T) {
e.ph.ps.rootDir = rootDir e.ph.ps.rootDir = rootDir
} }
e.rr = httptest.NewRecorder() 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) e.ph.ServeHTTP(e.rr, tt.req)
for _, f := range tt.checks { for _, f := range tt.checks {
f(t, &e) f(t, &e)
@ -455,6 +488,9 @@ func TestFileDeleteRace(t *testing.T) {
logf: t.Logf, logf: t.Logf,
capFileSharing: true, capFileSharing: true,
}, },
selfNode: &tailcfg.Node{
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")},
},
rootDir: dir, rootDir: dir,
} }
ph := &peerAPIHandler{ ph := &peerAPIHandler{
@ -467,7 +503,7 @@ func TestFileDeleteRace(t *testing.T) {
buf := make([]byte, 2<<20) buf := make([]byte, 2<<20)
for i := 0; i < 30; i++ { for i := 0; i < 30; i++ {
rr := httptest.NewRecorder() 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 { if res := rr.Result(); res.StatusCode != 200 {
t.Fatal(res.Status) t.Fatal(res.Status)
} }