From b533f852fa219ba28421ad9dcc6ef263f6f67a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Wed, 24 Jun 2026 17:44:02 -0300 Subject: [PATCH] fix(server): prevent /debug/* exposure via DefaultServeMux fallthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kratos' HTTP server routes unmatched requests to http.DefaultServeMux, where net/http/pprof, expvar and golang.org/x/net/trace auto-register handlers from their init() functions. As a result /debug/pprof/*, /debug/vars and /debug/requests were reachable on the public control-plane and CAS HTTP servers regardless of the enable_profiler flag. Add a DenyDefaultMuxFallthrough helper that overrides the Kratos NotFoundHandler and MethodNotAllowedHandler to return 404/405 instead of delegating to the global mux, and apply it to the public-facing HTTP and metrics servers in both the control plane and CAS. The dedicated :6060 profiler server is left unchanged so profiling remains available when enable_profiler is set. Signed-off-by: Matías Insaurralde --- app/artifact-cas/internal/server/http.go | 3 + .../internal/server/httpmetrics.go | 5 +- app/controlplane/internal/server/http.go | 3 + .../internal/server/httpmetrics.go | 5 +- pkg/middlewares/http/denymux.go | 45 +++++++++++ pkg/middlewares/http/denymux_test.go | 80 +++++++++++++++++++ 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 pkg/middlewares/http/denymux.go create mode 100644 pkg/middlewares/http/denymux_test.go diff --git a/app/artifact-cas/internal/server/http.go b/app/artifact-cas/internal/server/http.go index 5452f123c..f0d3cb188 100644 --- a/app/artifact-cas/internal/server/http.go +++ b/app/artifact-cas/internal/server/http.go @@ -40,6 +40,9 @@ func NewHTTPServer(c *conf.Server, authConf *conf.Auth, downloadSvc *service.Dow logging.Server(logger), ), } + // Stop unmatched routes from falling through to http.DefaultServeMux, + // which would expose /debug/vars and /debug/requests (CVE-2026-6993). + opts = append(opts, middlewares_http.DenyDefaultMuxFallthrough()...) if c.Http.Network != "" { opts = append(opts, http.Network(c.Http.Network)) } diff --git a/app/artifact-cas/internal/server/httpmetrics.go b/app/artifact-cas/internal/server/httpmetrics.go index b37b58576..0eb8bca4e 100644 --- a/app/artifact-cas/internal/server/httpmetrics.go +++ b/app/artifact-cas/internal/server/httpmetrics.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package server import ( "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/conf" + middlewares_http "github.com/chainloop-dev/chainloop/pkg/middlewares/http" "github.com/go-kratos/kratos/v2/middleware/recovery" "github.com/go-kratos/kratos/v2/transport/http" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -34,6 +35,8 @@ func NewHTTPMetricsServer(c *conf.Server) (*HTTPMetricsServer, error) { recovery.Recovery(), ), } + // Stop unmatched routes from falling through to http.DefaultServeMux (CVE-2026-6993). + opts = append(opts, middlewares_http.DenyDefaultMuxFallthrough()...) if c.HttpMetrics.Network != "" { opts = append(opts, http.Network(c.HttpMetrics.Network)) diff --git a/app/controlplane/internal/server/http.go b/app/controlplane/internal/server/http.go index 7e08bd54e..246a79022 100644 --- a/app/controlplane/internal/server/http.go +++ b/app/controlplane/internal/server/http.go @@ -47,6 +47,9 @@ func NewHTTPServer(opts *Opts, grpcSrv *grpc.Server) (*http.Server, error) { var serverOpts = []http.ServerOption{ http.Middleware(middlewares...), } + // Stop unmatched routes from falling through to http.DefaultServeMux, + // which would expose /debug/pprof, /debug/vars and /debug/requests (CVE-2026-6993). + serverOpts = append(serverOpts, middlewares_http.DenyDefaultMuxFallthrough()...) if v := opts.ServerConfig.Http.Network; v != "" { serverOpts = append(serverOpts, http.Network(v)) diff --git a/app/controlplane/internal/server/httpmetrics.go b/app/controlplane/internal/server/httpmetrics.go index 624b0fb3c..f9f3bfffa 100644 --- a/app/controlplane/internal/server/httpmetrics.go +++ b/app/controlplane/internal/server/httpmetrics.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package server import ( + middlewares_http "github.com/chainloop-dev/chainloop/pkg/middlewares/http" "github.com/go-kratos/kratos/v2/transport/http" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -28,6 +29,8 @@ type HTTPMetricsServer struct { // NewHTTPMetricsServer exposes the metrics endpoint in another port func NewHTTPMetricsServer(opts *Opts) (*HTTPMetricsServer, error) { var serverOpts = []http.ServerOption{} + // Stop unmatched routes from falling through to http.DefaultServeMux (CVE-2026-6993). + serverOpts = append(serverOpts, middlewares_http.DenyDefaultMuxFallthrough()...) if v := opts.ServerConfig.HttpMetrics.Network; v != "" { serverOpts = append(serverOpts, http.Network(v)) diff --git a/pkg/middlewares/http/denymux.go b/pkg/middlewares/http/denymux.go new file mode 100644 index 000000000..2ae0b7661 --- /dev/null +++ b/pkg/middlewares/http/denymux.go @@ -0,0 +1,45 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + nhttp "net/http" + + "github.com/go-kratos/kratos/v2/transport/http" +) + +// DenyDefaultMuxFallthrough returns Kratos server options that stop the HTTP +// server from routing unmatched requests to http.DefaultServeMux. +// +// By default Kratos sets the router's NotFoundHandler and MethodNotAllowedHandler +// to http.DefaultServeMux (CVE-2026-6993, CWE-441). Packages such as +// net/http/pprof, expvar and golang.org/x/net/trace auto-register handlers on +// that global mux from their init() functions, so any unmatched route on a +// public server leaks /debug/pprof/*, /debug/vars and /debug/requests. +// +// Returning a plain 404/405 instead severs that fallthrough on every network +// path. Registered routes are matched by the router and never reach these +// handlers, so legitimate endpoints are unaffected. +func DenyDefaultMuxFallthrough() []http.ServerOption { + return []http.ServerOption{ + http.NotFoundHandler(nhttp.HandlerFunc(func(w nhttp.ResponseWriter, _ *nhttp.Request) { + w.WriteHeader(nhttp.StatusNotFound) + })), + http.MethodNotAllowedHandler(nhttp.HandlerFunc(func(w nhttp.ResponseWriter, _ *nhttp.Request) { + w.WriteHeader(nhttp.StatusMethodNotAllowed) + })), + } +} diff --git a/pkg/middlewares/http/denymux_test.go b/pkg/middlewares/http/denymux_test.go new file mode 100644 index 000000000..e4ab34a79 --- /dev/null +++ b/pkg/middlewares/http/denymux_test.go @@ -0,0 +1,80 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + nhttp "net/http" + "net/http/httptest" + "testing" + + "github.com/go-kratos/kratos/v2/transport/http" + "github.com/stretchr/testify/assert" +) + +// register a sentinel handler on the global mux to emulate what net/http/pprof, +// expvar and x/net/trace do from their init() functions. +func init() { + nhttp.DefaultServeMux.HandleFunc("/debug/sentinel", func(w nhttp.ResponseWriter, _ *nhttp.Request) { + w.WriteHeader(nhttp.StatusOK) + _, _ = w.Write([]byte("LEAKED")) + }) +} + +func TestDenyDefaultMuxFallthrough(t *testing.T) { + testCases := []struct { + name string + opts []http.ServerOption + path string + wantStatus int + }{ + { + name: "without the option the request falls through to DefaultServeMux", + opts: nil, + path: "/debug/sentinel", + wantStatus: nhttp.StatusOK, + }, + { + name: "with the option an unmatched route returns 404", + opts: DenyDefaultMuxFallthrough(), + path: "/debug/sentinel", + wantStatus: nhttp.StatusNotFound, + }, + { + name: "with the option a registered route still works", + opts: DenyDefaultMuxFallthrough(), + path: "/healthz", + wantStatus: nhttp.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + srv := http.NewServer(tc.opts...) + srv.HandleFunc("/healthz", func(w nhttp.ResponseWriter, _ *nhttp.Request) { + w.WriteHeader(nhttp.StatusOK) + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(nhttp.MethodGet, tc.path, nil) + srv.ServeHTTP(rec, req) + + assert.Equal(t, tc.wantStatus, rec.Code) + if tc.path == "/debug/sentinel" && tc.wantStatus == nhttp.StatusNotFound { + assert.NotContains(t, rec.Body.String(), "LEAKED") + } + }) + } +}