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.
Bug: HasPathHandler() 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
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 {
if !configIn.HasPathHandler() {
return nil
}
if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
return nil
}
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
}
}
}
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).