tailscale/tailscale

Privilege escalation: HasPathHandler() skips Services, bypassing admin check for path handlers

Summary

  • Context: The HasPathHandler() method in ipn/serve.go determines whether a ServeConfig contains path handlers, which triggers authorization checks requiring admin privileges for filesystem access.

  • BugHasPathHandler() does not check the Services map for path handlers.

  • Actual vs. expected: The method only checks the top-level Web map and Foreground configs, but ignores path handlers configured within the Services map, causing it to incorrectly return false.

  • Impact: Non-admin users can serve arbitrary filesystem paths through Service configurations, bypassing the admin authorization requirement.

Code with bug

In ipn/serve.go, the HasPathHandler() method missing the Services check:

func (sc *ServeConfig) HasPathHandler() bool {
    if sc.Web != nil {
        for _, webServerConfig := range sc.Web {
            for _, httpHandler := range webServerConfig.Handlers {
                if httpHandler.Path != "" {
                    return true
                }
            }
        }
    }

    if sc.Foreground != nil {
        for _, fgConfig := range sc.Foreground {
            if fgConfig.HasPathHandler() {
                return true
            }
        }
    }

    return false // <-- BUG 🔴 Missing check for sc.Services

However, ServiceConfig contains a Web field that can contain path handlers:

type ServiceConfig struct {
    TCP map[uint16]*TCPPortHandler       `json:",omitempty"`
    Web map[HostPort]*WebServerConfig   `json:",omitempty"`
    Tun bool                            `json:",omitempty"`

The security check in ipn/localapi/serve.go relies on HasPathHandler():

func authorizeServeConfigForGOOSAndUserContext(
    goos string,
    configIn *ipn.ServeConfig,
    h *Handler,
) error {
    // ... platform checks ...

    if !configIn.HasPathHandler() {
        return nil // Bypasses authorization if HasPathHandler() returns false
    }

    if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
        return nil
    }

    // ... return error requiring admin privileges ...

Failing test

The following test demonstrates the bug:

func TestHasPathHandlerWithServices(t *testing.T) {
    config := &ServeConfig{
        Services: map[tailcfg.ServiceName]*ServiceConfig{
            "svc:myservice": {
                Web: map[HostPort]*WebServerConfig{
                    "myservice.example.com:443": {
                        Handlers: map[string]*HTTPHandler{
                            "/": {
                                Path: "/var/www",
                            },
                        },
                    },
                },
            },
        },
    }

    got := config.HasPathHandler()
    want := true

    if got != want {
        t.Errorf("HasPathHandler() = %v, want %v", got, want

Test output:

=== RUN   TestHasPathHandlerWithServices
    serve_haspathhandler_test.go:127:
        HasPathHandler() = false
        want true
--- FAIL: TestHasPathHandlerWithServices (0.00s)

The test confirms that HasPathHandler() returns false when a path handler exists in the Services map.

Recommended fix

Add a check for path handlers in the Services map:

func (sc *ServeConfig) HasPathHandler() bool {
    if sc.Web != nil {
        for _, webServerConfig := range sc.Web {
            for _, httpHandler := range webServerConfig.Handlers {
                if httpHandler.Path != "" {
                    return true
                }
            }
        }
    }

    if sc.Foreground != nil {
        for _, fgConfig := range sc.Foreground {
            if fgConfig.HasPathHandler() {
                return true
            }
        }
    }

    // <-- FIX 🟢 Add check for Services
    if sc.Services != nil {
        for _, svcConfig := range sc.Services {
            if svcConfig.Web != nil {
                for _, webServerConfig := range svcConfig.Web {
                    for _, httpHandler := range webServerConfig.Handlers {
                        if httpHandler.Path != "" {
                            return true
                        }
                    }
                }
            }
        }
    }

    return false

This ensures that path handlers in Service configurations are properly detected, triggering the required admin authorization checks on platforms where filesystem access restrictions apply (Windows, Linux, Darwin, Illumos, Solaris).