tailscale/tailscale
Privilege escalation: HasPathHandler() skips Services, bypassing admin check for path handlers
Summary
Context: The
HasPathHandler()method inipn/serve.godetermines whether a ServeConfig contains path handlers, which triggers authorization checks requiring admin privileges for filesystem access.Bug:
HasPathHandler()does not check theServicesmap for path handlers.Actual vs. expected: The method only checks the top-level
Webmap andForegroundconfigs, but ignores path handlers configured within theServicesmap, causing it to incorrectly returnfalse.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
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
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"`
type ServiceConfig struct { TCP map[uint16]*TCPPortHandler `json:",omitempty"` Web map[HostPort]*WebServerConfig `json:",omitempty"` Tun bool `json:",omitempty"`
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 ...
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 ...
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
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
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)
=== RUN TestHasPathHandlerWithServices serve_haspathhandler_test.go:127: HasPathHandler() = false want true --- FAIL: TestHasPathHandlerWithServices (0.00s)
=== 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
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
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).