Files
pulse/internal/api/demo_mode_operations.go
T
rcourtman 0180b33431 Hide pprof routes on demo-mode instances
The /debug/pprof handlers are gated on RequireAdmin plus a settings:read
scope that session logins skip. The public demo signs every visitor in
as the configured admin, so any visitor could fetch goroutine and heap
dumps and the process command line, or start CPU profiles and execution
traces that load the whole process. DEMO_MODE already hides diagnostics,
logs, admin users and manual discovery through the admin-operations
policy; pprof was missing from it.

The policy now has the demo guard answer /debug/pprof and every path
below it with 404 for every method, ahead of route authentication.
Instances outside demo mode keep the routes, and PULSE_PPROF_DISABLED
still removes them everywhere.

TestContract_DemoModeHidesPprofFromDemoAdminSession signs in as the
demo admin through the full router, shows the same session still reads
/debug/pprof/cmdline below the demo guard, and gets 404 for eight pprof
paths through the served handler. On the previous policy GET and HEAD
returned pprof output (200, or the root's 301 into the index) and POST
got the generic demo 403. A policy test also fails if a literal route
the router files register under /debug/, with or without a method
prefix, escapes the policy for any common method.
2026-10-07 20:04:07 +01:00

84 lines
2.5 KiB
Go

package api
import (
"net/http"
"strings"
)
var publicDemoAdminOperationsPolicies = []publicDemoCommercialRoutePolicy{
{
route: "GET /api/diagnostics",
exposure: publicDemoCommercialExposureHidden,
matches: readDemoCommercialPath("/api/diagnostics"),
},
{
route: "POST /api/diagnostics/docker/prepare-token",
exposure: publicDemoCommercialExposureHidden,
matches: exactDemoCommercialMethodPath(http.MethodPost, "/api/diagnostics/docker/prepare-token"),
},
{
route: "GET /api/logs/stream",
exposure: publicDemoCommercialExposureHidden,
matches: readDemoCommercialPath("/api/logs/stream"),
},
{
route: "GET /api/logs/download",
exposure: publicDemoCommercialExposureHidden,
matches: readDemoCommercialPath("/api/logs/download"),
},
{
route: "GET /api/logs/level",
exposure: publicDemoCommercialExposureHidden,
matches: readDemoCommercialPath("/api/logs/level"),
},
{
route: "POST /api/logs/level",
exposure: publicDemoCommercialExposureHidden,
matches: exactDemoCommercialMethodPath(http.MethodPost, "/api/logs/level"),
},
{
route: "GET /api/admin/users",
exposure: publicDemoCommercialExposureHidden,
matches: readDemoCommercialPath("/api/admin/users"),
},
{
route: "GET /api/discover",
exposure: publicDemoCommercialExposureHidden,
matches: readDemoCommercialPath("/api/discover"),
},
// Go runtime profiling. These routes are gated only on admin auth, which
// a demo login that is the configured admin passes, so every visitor could
// otherwise pull goroutine and heap dumps or the command line, or start
// CPU profiles and traces that cost the whole process. Hidden for every
// method, so a write probe such as the POST symbol lookup gets the same
// 404 instead of revealing the route.
{
route: "/debug/pprof",
exposure: publicDemoCommercialExposureHidden,
matches: familyDemoCommercialPath("/debug/pprof"),
},
}
func publicDemoAdminOperationsPolicyForRequest(
r *http.Request,
) (publicDemoCommercialExposure, bool) {
for _, policy := range publicDemoAdminOperationsPolicies {
if policy.matches != nil && policy.matches(r) {
return policy.exposure, true
}
}
return "", false
}
// familyDemoCommercialPath matches root and every path below it, for any
// method.
func familyDemoCommercialPath(root string) func(*http.Request) bool {
return func(r *http.Request) bool {
if r == nil || r.URL == nil {
return false
}
path := normalizeDemoCommercialPath(r.URL.Path)
return path == root || strings.HasPrefix(path, root+"/")
}
}