diff --git a/client/tailscale/apitype/apitype.go b/client/tailscale/apitype/apitype.go index d10e20533..14b82cbbf 100644 --- a/client/tailscale/apitype/apitype.go +++ b/client/tailscale/apitype/apitype.go @@ -7,6 +7,9 @@ package apitype import "tailscale.com/tailcfg" +// LocalAPIHost is the Host header value used by the LocalAPI. +const LocalAPIHost = "local-tailscaled.sock" + // WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler. type WhoIsResponse struct { Node *tailcfg.Node diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index a9e6edc28..30772db24 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -200,7 +200,7 @@ func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus if jr, ok := body.(jsonReader); ok && jr.err != nil { return nil, jr.err // fail early if there was a JSON marshaling error } - req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body) + req, err := http.NewRequestWithContext(ctx, method, "http://"+apitype.LocalAPIHost+path, body) if err != nil { return nil, err } @@ -440,7 +440,7 @@ func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) e } func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) { - req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil) + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/files/"+url.PathEscape(baseName), nil) if err != nil { return nil, 0, err } @@ -473,7 +473,7 @@ func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, e // A size of -1 means unknown. // The name parameter is the original filename, not escaped. func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error { - req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r) + req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+apitype.LocalAPIHost+"/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r) if err != nil { return err } @@ -584,7 +584,7 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n }, } ctx = httptrace.WithClientTrace(ctx, &trace) - req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/dial", nil) + req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/dial", nil) if err != nil { return nil, err } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index c04b716b5..d817ec56c 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -34,6 +34,7 @@ import ( "tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnstate" "tailscale.com/net/netutil" + "tailscale.com/safesocket" "tailscale.com/tailcfg" "tailscale.com/tka" "tailscale.com/types/key" @@ -137,6 +138,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "server has no local backend", http.StatusInternalServerError) return } + if r.Referer() != "" || r.Header.Get("Origin") != "" || !validHost(r.Host) { + http.Error(w, "invalid localapi request", http.StatusForbidden) + return + } w.Header().Set("Tailscale-Version", version.Long) if h.RequiredPassword != "" { _, pass, ok := r.BasicAuth() @@ -156,6 +161,24 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +// validHost reports whether h is a valid Host header value for a LocalAPI request. +func validHost(h string) bool { + // The client code sends a hostname of "local-tailscaled.sock". + switch h { + case "", apitype.LocalAPIHost: + return true + } + // Otherwise, any Host header we see should at most be an ip:port. + ap, err := netip.ParseAddrPort(h) + if err != nil { + return false + } + if runtime.GOOS == "windows" && ap.Port() != safesocket.WindowsLocalPort { + return false + } + return ap.Addr().IsLoopback() +} + // handlerForPath returns the LocalAPI handler for the provided Request.URI.Path. // (the path doesn't include any query parameters) func handlerForPath(urlPath string) (h localAPIHandler, ok bool) {