mirror of
https://github.com/thecodingrobot/echoip.git
synced 2026-05-09 00:04:23 +02:00
security: split pprof onto private listener and add HTTP timeouts
The -P flag previously mounted /debug/pprof and /debug/cache on the public :8080 listener. pprof.Profile pegs a CPU for 30s (unauthenticated DoS) and /debug/cache/resize was world-writable. - -P changes from bool to string addr (e.g. 127.0.0.1:6060). When set, a second http.Server serves DebugHandler() on that addr. - Public ListenAndServe now uses an explicit *http.Server with ReadHeaderTimeout=5s, ReadTimeout=10s, WriteTimeout=15s, IdleTimeout=60s to mitigate slowloris. - http.New() no longer takes the profile bool. BREAKING: -P now requires an address argument.
This commit is contained in:
parent
40e66943cc
commit
27957c928f
3 changed files with 88 additions and 28 deletions
|
|
@ -37,7 +37,7 @@ func main() {
|
|||
portLookup := flag.Bool("p", false, "Enable port lookup")
|
||||
template := flag.String("t", "html", "Path to template dir")
|
||||
cacheSize := flag.Int("C", 0, "Size of response cache. Set to 0 to disable")
|
||||
profile := flag.Bool("P", false, "Enables profiling handlers")
|
||||
profileAddr := flag.String("P", "", "Listening address for debug/profiling handlers (e.g. 127.0.0.1:6060). Empty disables them. NEVER expose this to the public internet: it serves pprof (which can pin a CPU for 30s) and a cache resize POST endpoint.")
|
||||
sponsor := flag.Bool("s", false, "Show sponsor logo")
|
||||
var headers multiValueFlag
|
||||
flag.Var(&headers, "H", "Header to trust for remote IP, if present (e.g. X-Real-IP)")
|
||||
|
|
@ -52,10 +52,15 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
cache := http.NewCache(*cacheSize)
|
||||
server := http.New(r, cache, *profile)
|
||||
server := http.New(r, cache)
|
||||
server.IPHeaders = headers
|
||||
if _, err := os.Stat(*template); err == nil {
|
||||
server.Template = *template
|
||||
if *template != "" {
|
||||
if err := server.LoadTemplates(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("Not configuring default handler: Template not found: %s", *template)
|
||||
}
|
||||
|
|
@ -77,8 +82,13 @@ func main() {
|
|||
if *cacheSize > 0 {
|
||||
log.Printf("Cache capacity set to %d", *cacheSize)
|
||||
}
|
||||
if *profile {
|
||||
log.Printf("Enabling profiling handlers")
|
||||
if *profileAddr != "" {
|
||||
log.Printf("Enabling debug/profiling handlers on http://%s (do not expose publicly)", *profileAddr)
|
||||
go func(addr string) {
|
||||
if err := server.ListenAndServeDebug(addr); err != nil {
|
||||
log.Fatalf("debug listener on %s failed: %s", addr, err)
|
||||
}
|
||||
}(*profileAddr)
|
||||
}
|
||||
log.Printf("Listening on http://%s", *listen)
|
||||
if err := server.ListenAndServe(*listen); err != nil {
|
||||
|
|
|
|||
88
http/http.go
88
http/http.go
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"net/http/pprof"
|
||||
"net/netip"
|
||||
|
|
@ -34,8 +35,8 @@ type Server struct {
|
|||
LookupPort func(netip.Addr, uint64) error
|
||||
cache *Cache
|
||||
gr geo.Reader
|
||||
profile bool
|
||||
Sponsor bool
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
|
|
@ -63,8 +64,24 @@ type PortResponse struct {
|
|||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
func New(db geo.Reader, cache *Cache, profile bool) *Server {
|
||||
return &Server{cache: cache, gr: db, profile: profile}
|
||||
func New(db geo.Reader, cache *Cache) *Server {
|
||||
return &Server{cache: cache, gr: db}
|
||||
}
|
||||
|
||||
// LoadTemplates parses all templates under s.Template once and caches the
|
||||
// resulting *template.Template on the server. Callers should invoke this
|
||||
// after setting Template and before serving requests, to avoid re-parsing
|
||||
// the template directory on every browser hit.
|
||||
func (s *Server) LoadTemplates() error {
|
||||
if s.Template == "" {
|
||||
return fmt.Errorf("template directory is not set")
|
||||
}
|
||||
t, err := template.ParseGlob(s.Template + "/*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing templates in %s: %w", s.Template, err)
|
||||
}
|
||||
s.tmpl = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipFromForwardedForHeader(v string) string {
|
||||
|
|
@ -327,14 +344,13 @@ func (s *Server) cacheHandler(w http.ResponseWriter, r *http.Request) *appError
|
|||
}
|
||||
|
||||
func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
if s.tmpl == nil {
|
||||
return notFound(nil).WithMessage("404 page not found")
|
||||
}
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error())
|
||||
}
|
||||
t, err := template.ParseGlob(s.Template + "/*")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
json, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
|
|
@ -361,7 +377,7 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro
|
|||
s.LookupPort != nil,
|
||||
s.Sponsor,
|
||||
}
|
||||
if err := t.Execute(w, &data); err != nil {
|
||||
if err := s.tmpl.Execute(w, &data); err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -433,11 +449,15 @@ func (s *Server) Handler() http.Handler {
|
|||
r.Route("GET", "/", s.CLIHandler).MatcherFunc(cliMatcher)
|
||||
r.Route("GET", "/", s.CLIHandler).Header("Accept", textMediaType)
|
||||
r.Route("GET", "/ip", s.CLIHandler)
|
||||
if !s.gr.IsEmpty() {
|
||||
if s.gr.HasCountry() {
|
||||
r.Route("GET", "/country", s.CLICountryHandler)
|
||||
r.Route("GET", "/country-iso", s.CLICountryISOHandler)
|
||||
}
|
||||
if s.gr.HasCity() {
|
||||
r.Route("GET", "/city", s.CLICityHandler)
|
||||
r.Route("GET", "/coordinates", s.CLICoordinatesHandler)
|
||||
}
|
||||
if s.gr.HasASN() {
|
||||
r.Route("GET", "/asn", s.CLIASNHandler)
|
||||
r.Route("GET", "/asn-org", s.CLIASNOrgHandler)
|
||||
}
|
||||
|
|
@ -452,22 +472,48 @@ func (s *Server) Handler() http.Handler {
|
|||
r.RoutePrefix("GET", "/port/", s.PortHandler)
|
||||
}
|
||||
|
||||
// Profiling
|
||||
if s.profile {
|
||||
r.Route("POST", "/debug/cache/resize", s.cacheResizeHandler)
|
||||
r.Route("GET", "/debug/cache/", s.cacheHandler)
|
||||
r.Route("GET", "/debug/pprof/cmdline", wrapHandlerFunc(pprof.Cmdline))
|
||||
r.Route("GET", "/debug/pprof/profile", wrapHandlerFunc(pprof.Profile))
|
||||
r.Route("GET", "/debug/pprof/symbol", wrapHandlerFunc(pprof.Symbol))
|
||||
r.Route("GET", "/debug/pprof/trace", wrapHandlerFunc(pprof.Trace))
|
||||
r.RoutePrefix("GET", "/debug/pprof/", wrapHandlerFunc(pprof.Index))
|
||||
}
|
||||
|
||||
return r.Handler()
|
||||
}
|
||||
|
||||
// DebugHandler returns an http.Handler exposing pprof and cache debug
|
||||
// endpoints. These routes leak runtime information and include a POST endpoint
|
||||
// (/debug/cache/resize) plus pprof.Profile, which can pin a CPU for the
|
||||
// duration of a profile capture. The returned handler must only be served on
|
||||
// a private listener (e.g. loopback) and never exposed to the public internet.
|
||||
func (s *Server) DebugHandler() http.Handler {
|
||||
r := NewRouter()
|
||||
r.Route("POST", "/debug/cache/resize", s.cacheResizeHandler)
|
||||
r.Route("GET", "/debug/cache/", s.cacheHandler)
|
||||
r.Route("GET", "/debug/pprof/cmdline", wrapHandlerFunc(pprof.Cmdline))
|
||||
r.Route("GET", "/debug/pprof/profile", wrapHandlerFunc(pprof.Profile))
|
||||
r.Route("GET", "/debug/pprof/symbol", wrapHandlerFunc(pprof.Symbol))
|
||||
r.Route("GET", "/debug/pprof/trace", wrapHandlerFunc(pprof.Trace))
|
||||
r.RoutePrefix("GET", "/debug/pprof/", wrapHandlerFunc(pprof.Index))
|
||||
return r.Handler()
|
||||
}
|
||||
|
||||
// newServer returns an *http.Server with conservative timeouts to mitigate
|
||||
// slowloris-style resource exhaustion attacks.
|
||||
func newServer(addr string, h http.Handler) *http.Server {
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
Handler: h,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
return http.ListenAndServe(addr, s.Handler())
|
||||
return newServer(addr, s.Handler()).ListenAndServe()
|
||||
}
|
||||
|
||||
// ListenAndServeDebug starts an HTTP server bound to addr that serves the
|
||||
// debug handler. The caller is responsible for ensuring addr is not reachable
|
||||
// from untrusted networks.
|
||||
func (s *Server) ListenAndServeDebug(addr string) error {
|
||||
return newServer(addr, s.DebugHandler()).ListenAndServe()
|
||||
}
|
||||
|
||||
func formatCoordinate(c float64) string {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ func (t *testDb) ASN(netip.Addr) (geo.ASN, error) {
|
|||
|
||||
func (t *testDb) IsEmpty() bool { return false }
|
||||
|
||||
func (t *testDb) HasCountry() bool { return true }
|
||||
|
||||
func (t *testDb) HasCity() bool { return true }
|
||||
|
||||
func (t *testDb) HasASN() bool { return true }
|
||||
|
||||
func testServer() *Server {
|
||||
return &Server{cache: NewCache(100), gr: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort}
|
||||
}
|
||||
|
|
@ -182,8 +188,7 @@ func TestJSONHandlers(t *testing.T) {
|
|||
func TestCacheHandler(t *testing.T) {
|
||||
log.SetOutput(io.Discard)
|
||||
srv := testServer()
|
||||
srv.profile = true
|
||||
s := httptest.NewServer(srv.Handler())
|
||||
s := httptest.NewServer(srv.DebugHandler())
|
||||
got, _, err := httpGet(s.URL+"/debug/cache/", jsonMediaType, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -197,8 +202,7 @@ func TestCacheHandler(t *testing.T) {
|
|||
func TestCacheResizeHandler(t *testing.T) {
|
||||
log.SetOutput(io.Discard)
|
||||
srv := testServer()
|
||||
srv.profile = true
|
||||
s := httptest.NewServer(srv.Handler())
|
||||
s := httptest.NewServer(srv.DebugHandler())
|
||||
_, got, err := httpPost(s.URL+"/debug/cache/resize", "10")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue