Description
Importing net/http/pprof automatically registers /debug/pprof/ handlers on http.DefaultServeMux, including with a named import. Serving that default mux on all interfaces with http.ListenAndServe(":8080", nil) and no separate access controls lets anyone who can reach the port inspect profiles without authentication.
Endpoints such as /debug/pprof, /debug/pprof/goroutine and /debug/pprof/heap can expose goroutines, memory state, internal paths and some request context. This is not itself code execution, but the information can help attackers identify handlers, infer secrets or understand service structure.
Potential impact
- Goroutine, heap and thread-creation profiles can reveal internal structure and execution state.
- Memory use, processing patterns and some internal paths may disclose infrastructure information.
- Attackers may use the information to investigate vulnerable endpoints or bottlenecks and prepare further attacks, including denial of service.
Remediation
- Bind pprof to
localhostor another loopback address, and use a separatehttp.NewServeMux()for the public service. - Register required pprof handlers on an internal mux, remembering that the import also registers handlers on the default mux.
- Restrict access with controls such as mTLS, a VPN or an internal proxy. If password authentication is used, also require verified TLS.
- Remove unnecessary production pprof imports and server settings; enable only required diagnostics with restricted access.
- Block external access with firewalls, security groups and Ingress controls, and check that a proxy does not expose internal pprof again.
Examples
Before
go
package main
import (
"log"
"net/http"
_ "net/http/pprof" // Register pprof handlers automatically on the default mux
)
func main() {
// pprof and application handlers share the default mux (nil)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
// Listening on all interfaces (:8080) also exposes /debug/pprof to reachable clients
log.Fatal(http.ListenAndServe(":8080", nil))
}
After
go
package main
import (
"log"
"net/http"
"net/http/pprof"
)
// Example of an internal profiling server
func startInternalPprofServer() {
mux := http.NewServeMux()
// Register pprof handlers on the internal server mux
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
// Bind only to a local address, such as localhost
go func() {
log.Println("internal pprof listening on localhost:6060")
if err := http.ListenAndServe("localhost:6060", mux); err != nil {
log.Println("pprof server stopped:", err)
}
}()
}
func main() {
// Start internal pprof; apply firewall/security-group access controls in production
startInternalPprofServer()
// pprof registers on the default mux, so use a separate service mux
publicMux := http.NewServeMux()
publicMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
// Serve the public port with a mux that has no pprof handlers
log.Fatal(http.ListenAndServe(":8080", publicMux))
}
Explanation:
- Before: The default mux serves pprof on the application port. Anyone who can reach that port can inspect profiles without separate authentication.
- After: The import still registers handlers on the default mux, but the public server uses only its separate
publicMux. The profiling mux listens onlocalhost:6060. Also control access by local users, processes in the same network namespace and any proxy that could expose it.