Exposed pprof debugging endpoints

Exposed pprof debugging endpoints

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 localhost or another loopback address, and use a separate http.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 on localhost:6060. Also control access by local users, processes in the same network namespace and any proxy that could expose it.

References