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 07319e277..8933675ec 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -197,7 +197,7 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) { } func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, 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 } @@ -435,7 +435,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 } @@ -472,7 +472,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 } @@ -595,7 +595,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 114eb1956..1535dc5e1 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -32,6 +32,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/logger" @@ -129,6 +130,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() @@ -148,6 +153,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) {