From 57679330bbee8a4ec7a76049ed423933ba1c1bab Mon Sep 17 00:00:00 2001 From: Tommy Situ Date: Tue, 9 Sep 2025 22:47:23 +0100 Subject: [PATCH 1/6] Disabled set middleware api by default --- core/admin.go | 2 +- core/cmd/hoverfly/main.go | 6 ++++++ core/handlers/v2/hoverfly_middleware_handler.go | 17 ++++++++++++----- .../v2/hoverfly_middleware_handler_test.go | 2 +- core/settings.go | 6 +++++- .../core/api/middleware_api_v2_test.go | 2 +- functional-tests/core/ft_simulate_mode_test.go | 2 +- functional-tests/hoverctl/middleware_test.go | 2 +- functional-tests/hoverctl/status_test.go | 2 +- 9 files changed, 29 insertions(+), 12 deletions(-) diff --git a/core/admin.go b/core/admin.go index fa434f774..6126eb41d 100644 --- a/core/admin.go +++ b/core/admin.go @@ -99,7 +99,7 @@ func getAllHandlers(hoverfly *Hoverfly) []handlers.AdminHandler { &v2.HoverflyHandler{Hoverfly: hoverfly}, &v2.HoverflyDestinationHandler{Hoverfly: hoverfly}, &v2.HoverflyModeHandler{Hoverfly: hoverfly}, - &v2.HoverflyMiddlewareHandler{Hoverfly: hoverfly}, + &v2.HoverflyMiddlewareHandler{Hoverfly: hoverfly, Enabled: hoverfly.Cfg.EnableMiddlewareAPI}, &v2.HoverflyUsageHandler{Hoverfly: hoverfly}, &v2.HoverflyVersionHandler{Hoverfly: hoverfly}, &v2.HoverflyUpstreamProxyHandler{Hoverfly: hoverfly}, diff --git a/core/cmd/hoverfly/main.go b/core/cmd/hoverfly/main.go index b663282df..be9270ba3 100644 --- a/core/cmd/hoverfly/main.go +++ b/core/cmd/hoverfly/main.go @@ -121,6 +121,9 @@ var ( cors = flag.Bool("cors", false, "Enable CORS support") noImportCheck = flag.Bool("no-import-check", false, "Skip duplicate request check when importing simulations") + // Feature flags + enableMiddlewareAPI = flag.Bool("enable-middleware-api", false, "Enable the admin API to set middleware (PUT /api/v2/hoverfly/middleware)") + pacFile = flag.String("pac-file", "", "Path to the pac file to be imported on startup") clientAuthenticationDestination = flag.String("client-authentication-destination", "", "Regular expression of destination with client authentication") @@ -403,6 +406,9 @@ func main() { log.Info("Import check has been disabled") } + // Feature flags + cfg.EnableMiddlewareAPI = *enableMiddlewareAPI + cfg.ClientAuthenticationDestination = *clientAuthenticationDestination cfg.ClientAuthenticationClientCert = *clientAuthenticationClientCert cfg.ClientAuthenticationClientKey = *clientAuthenticationClientKey diff --git a/core/handlers/v2/hoverfly_middleware_handler.go b/core/handlers/v2/hoverfly_middleware_handler.go index 5281bd684..ff127a855 100644 --- a/core/handlers/v2/hoverfly_middleware_handler.go +++ b/core/handlers/v2/hoverfly_middleware_handler.go @@ -16,6 +16,7 @@ type HoverflyMiddleware interface { type HoverflyMiddlewareHandler struct { Hoverfly HoverflyMiddleware + Enabled bool } func (this *HoverflyMiddlewareHandler) RegisterRoutes(mux *bone.Mux, am *handlers.AuthHandler) { @@ -24,10 +25,12 @@ func (this *HoverflyMiddlewareHandler) RegisterRoutes(mux *bone.Mux, am *handler negroni.HandlerFunc(this.Get), )) - mux.Put("/api/v2/hoverfly/middleware", negroni.New( - negroni.HandlerFunc(am.RequireTokenAuthentication), - negroni.HandlerFunc(this.Put), - )) + if this.Enabled { + mux.Put("/api/v2/hoverfly/middleware", negroni.New( + negroni.HandlerFunc(am.RequireTokenAuthentication), + negroni.HandlerFunc(this.Put), + )) + } mux.Options("/api/v2/hoverfly/middleware", negroni.New( negroni.HandlerFunc(this.Options), )) @@ -60,6 +63,10 @@ func (this *HoverflyMiddlewareHandler) Put(w http.ResponseWriter, req *http.Requ } func (this *HoverflyMiddlewareHandler) Options(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - w.Header().Add("Allow", "OPTIONS, GET, PUT") + allow := "OPTIONS, GET" + if this.Enabled { + allow += ", PUT" + } + w.Header().Add("Allow", allow) handlers.WriteResponse(w, []byte("")) } diff --git a/core/handlers/v2/hoverfly_middleware_handler_test.go b/core/handlers/v2/hoverfly_middleware_handler_test.go index d182c688d..441e187bb 100644 --- a/core/handlers/v2/hoverfly_middleware_handler_test.go +++ b/core/handlers/v2/hoverfly_middleware_handler_test.go @@ -130,7 +130,7 @@ func Test_HoverflyMiddlewareHandler_Options_GetsOptions(t *testing.T) { RegisterTestingT(t) var stubHoverfly HoverflyMiddlewareStub - unit := HoverflyMiddlewareHandler{Hoverfly: &stubHoverfly} + unit := HoverflyMiddlewareHandler{Hoverfly: &stubHoverfly, Enabled: true} request, err := http.NewRequest("OPTIONS", "/api/v2/hoverfly/middleware", nil) Expect(err).To(BeNil()) diff --git a/core/settings.go b/core/settings.go index 88851ad06..835dab30a 100644 --- a/core/settings.go +++ b/core/settings.go @@ -1,11 +1,12 @@ package hoverfly import ( - "github.com/SpectoLabs/hoverfly/core/cors" "os" "strconv" "sync" + "github.com/SpectoLabs/hoverfly/core/cors" + "strings" "github.com/SpectoLabs/hoverfly/core/middleware" @@ -52,6 +53,9 @@ type Configuration struct { ResponsesBodyFilesPath string ResponsesBodyFilesAllowedOrigins []string + // Feature flags + EnableMiddlewareAPI bool + ProxyControlWG sync.WaitGroup mu sync.Mutex diff --git a/functional-tests/core/api/middleware_api_v2_test.go b/functional-tests/core/api/middleware_api_v2_test.go index 47f132407..474fb5600 100644 --- a/functional-tests/core/api/middleware_api_v2_test.go +++ b/functional-tests/core/api/middleware_api_v2_test.go @@ -18,7 +18,7 @@ var _ = Describe("/api/v2/hoverfly/middleware", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") }) AfterEach(func() { diff --git a/functional-tests/core/ft_simulate_mode_test.go b/functional-tests/core/ft_simulate_mode_test.go index 1281f6e66..23769a26a 100644 --- a/functional-tests/core/ft_simulate_mode_test.go +++ b/functional-tests/core/ft_simulate_mode_test.go @@ -21,7 +21,7 @@ var _ = Describe("When I run Hoverfly in simulate mode", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") hoverfly.SetMode("simulate") }) diff --git a/functional-tests/hoverctl/middleware_test.go b/functional-tests/hoverctl/middleware_test.go index c2e5b3e4d..a6ed694ab 100644 --- a/functional-tests/hoverctl/middleware_test.go +++ b/functional-tests/hoverctl/middleware_test.go @@ -23,7 +23,7 @@ var _ = Describe("When I use hoverctl", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") hoverfly.SetMiddleware("ruby", "#!/usr/bin/env ruby\n# encoding: utf-8\nwhile payload = STDIN.gets\nnext unless payload\n\nSTDOUT.puts payload\nend") functional_tests.Run(hoverctlBinary, "targets", "update", "local", "--admin-port", hoverfly.GetAdminPort()) diff --git a/functional-tests/hoverctl/status_test.go b/functional-tests/hoverctl/status_test.go index ff155b316..74aa5d045 100644 --- a/functional-tests/hoverctl/status_test.go +++ b/functional-tests/hoverctl/status_test.go @@ -16,7 +16,7 @@ var _ = Describe("when I use hoverctl status", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") functional_tests.Run(hoverctlBinary, "targets", "update", "local", "--admin-port", hoverfly.GetAdminPort()) }) From bb9ae387466cfe7260f414489e857b78682cdec2 Mon Sep 17 00:00:00 2001 From: Tommy Situ Date: Tue, 9 Sep 2025 22:53:24 +0100 Subject: [PATCH 2/6] Add a functional test for the middleware api --- .../core/api/middleware_api_v2_test.go | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/functional-tests/core/api/middleware_api_v2_test.go b/functional-tests/core/api/middleware_api_v2_test.go index 474fb5600..2fb2069ce 100644 --- a/functional-tests/core/api/middleware_api_v2_test.go +++ b/functional-tests/core/api/middleware_api_v2_test.go @@ -56,3 +56,30 @@ var _ = Describe("/api/v2/hoverfly/middleware", func() { }) }) }) + + +var _ = Describe("/api/v2/hoverfly/middleware when disabled", func() { + + var ( + hoverfly *functional_tests.Hoverfly + ) + + BeforeEach(func() { + hoverfly = functional_tests.NewHoverfly() + hoverfly.Start() + }) + + AfterEach(func() { + hoverfly.Stop() + }) + + Context("PUT", func() { + + It("Should return 404 when setting middleware", func() { + req := sling.New().Put("http://localhost:" + hoverfly.GetAdminPort() + "/api/v2/hoverfly/middleware") + req.Body(strings.NewReader(`{"binary":"ruby","script":"puts 'hi'"}`)) + res := functional_tests.DoRequest(req) + Expect(res.StatusCode).To(Equal(404)) + }) + }) +}) From 83ffaca71d87b13cc16550dd166ac3dba3000ab1 Mon Sep 17 00:00:00 2001 From: Tommy Situ Date: Tue, 9 Sep 2025 23:12:43 +0100 Subject: [PATCH 3/6] add start option for hoverctl to enable middleware api --- functional-tests/hoverctl/start_test.go | 23 ++++++++++++++++++++--- hoverctl/cmd/start.go | 6 ++++++ hoverctl/configuration/target.go | 8 ++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/functional-tests/hoverctl/start_test.go b/functional-tests/hoverctl/start_test.go index 4cbbda920..aaa91a4e9 100644 --- a/functional-tests/hoverctl/start_test.go +++ b/functional-tests/hoverctl/start_test.go @@ -1,9 +1,12 @@ package hoverctl_suite import ( - "github.com/antonholmquist/jason" + "io" "io/ioutil" "strconv" + "strings" + + "github.com/antonholmquist/jason" "github.com/SpectoLabs/hoverfly/core/authentication/backends" "github.com/SpectoLabs/hoverfly/functional-tests" @@ -327,7 +330,21 @@ var _ = Describe("hoverctl `start`", func() { }) }) - Context("with pac-file", func() { + Context("with enable-middleware-api", func() { + It("enables middleware API when flag is provided", func() { + output := functional_tests.Run(hoverctlBinary, "start", "--enable-middleware-api") + Expect(output).To(ContainSubstring("Hoverfly is now running")) + + // Attempt to set middleware via Admin API should be allowed (200) + req := sling.New().Put("http://localhost:8888/api/v2/hoverfly/middleware") + // Use a valid echo middleware that reads from STDIN and outputs the JSON payload unchanged + req.Body(strings.NewReader(`{"binary":"ruby", "script":"#!/usr/bin/env ruby\n# encoding: utf-8\nwhile payload = STDIN.gets\nnext unless payload\n\nSTDOUT.puts payload\nend"}`)) + res := functional_tests.DoRequest(req) + Expect(res.StatusCode).To(Equal(200)) + }) + }) + + Context("with pac-file", func() { BeforeEach(func() { }) @@ -338,7 +355,7 @@ var _ = Describe("hoverctl `start`", func() { response := functional_tests.DoRequest(sling.New().Get("http://localhost:8888/api/v2/hoverfly/pac")) Expect(response.StatusCode).To(Equal(200)) - responseBody, err := ioutil.ReadAll(response.Body) + responseBody, err := io.ReadAll(response.Body) Expect(err).To(BeNil()) Expect(string(responseBody)).To(ContainSubstring(`function FindProxyForURL(url, host) {`)) }) diff --git a/hoverctl/cmd/start.go b/hoverctl/cmd/start.go index 1cb061052..8b138211b 100644 --- a/hoverctl/cmd/start.go +++ b/hoverctl/cmd/start.go @@ -78,6 +78,9 @@ hoverctl configuration file. target.Simulations, _ = cmd.Flags().GetStringSlice("import") + // Feature flags + target.EnableMiddlewareAPI, _ = cmd.Flags().GetBool("enable-middleware-api") + if pacFileLocation, _ := cmd.Flags().GetString("pac-file"); pacFileLocation != "" { pacFileData, err := configuration.ReadFile(pacFileLocation) @@ -199,4 +202,7 @@ func init() { startCmd.Flags().StringSlice("logs-output", []string{}, "Locations for log output, \"console\"(default) or \"file\"") startCmd.Flags().String("logs-file", "", "Log file name. Use \"hoverfly-.log\" if not provided") startCmd.Flags().String("log-level", "info", "Set log level (panic, fatal, error, warn, info or debug)") + + // Feature flags + startCmd.Flags().Bool("enable-middleware-api", false, "Enable the admin API to set middleware (PUT /api/v2/hoverfly/middleware)") } diff --git a/hoverctl/configuration/target.go b/hoverctl/configuration/target.go index 4a39bece1..0bd251259 100644 --- a/hoverctl/configuration/target.go +++ b/hoverctl/configuration/target.go @@ -33,6 +33,9 @@ type Target struct { ClientAuthenticationClientKey string `yaml:",omitempty"` ClientAuthenticationCACert string `yaml:",omitempty"` + // Feature flags + EnableMiddlewareAPI bool `yaml:",omitempty"` + AuthEnabled bool Username string Password string @@ -171,5 +174,10 @@ func (this Target) BuildFlags() Flags { } } + // Feature flags + if this.EnableMiddlewareAPI { + flags = append(flags, "-enable-middleware-api") + } + return flags } From a49052205343a1098b02b78f8441f0887101c715 Mon Sep 17 00:00:00 2001 From: Tommy Situ Date: Tue, 9 Sep 2025 23:30:44 +0100 Subject: [PATCH 4/6] Update docs around security implication for exposing set middleware API --- docs/pages/keyconcepts/middleware.rst | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/pages/keyconcepts/middleware.rst b/docs/pages/keyconcepts/middleware.rst index e96678045..0fc68df52 100644 --- a/docs/pages/keyconcepts/middleware.rst +++ b/docs/pages/keyconcepts/middleware.rst @@ -25,13 +25,13 @@ You can write middleware in any language. There are two different types of middl Local Middleware ---------------- -Hoverfly has the ability to invoke middleware by executing a script or binary file on a host operating system. +Hoverfly has the ability to invoke middleware by executing a script or binary file on a host operating system. The only requires are that the provided middleware can be executed and sends the Middleware JSON schema to stdout when the Middleware JSON schema is received on stdin. HTTP Middleware --------------- -Hoverfly can also send middleware requests to a HTTP server instead of running a process locally. The benefits of this +Hoverfly can also send middleware requests to a HTTP server instead of running a process locally. The benefits of this are that Hoverfly does not initiate the process, giving more control to the user. The only requirements are that Hoverfly can POST the Middleware JSON schema to middleware URL provided and the middleware HTTP server responses with a 200 and the Middleware JSON schema is in the response. @@ -50,3 +50,30 @@ Hoverfly will send the JSON object to middleware via the standard input stream. .. seealso:: Middleware examples are covered in the tutorials section. See :ref:`randomlatency` and :ref:`modifyingresponses`. + + +Security and availability of the Set Middleware API +--------------------------------------------------- + +By default, the admin endpoint to set middleware (PUT /api/v2/hoverfly/middleware) is disabled. To enable it: + +- When starting Hoverfly directly: run with the flag: -enable-middleware-api +- When starting via hoverctl: use the same flag on start: hoverctl start --enable-middleware-api + +Network binding and remote access +--------------------------------- + +By default, Hoverfly binds its Admin and Proxy ports to the loopback interface only (127.0.0.1). This means the Admin API is not reachable from remote hosts out of the box. + +.. warning:: + + Exposing the Admin API outside localhost increases risk, especially if the Set Middleware API is enabled, because it allows executing arbitrary scripts/binaries on the host (for local middleware) or invoking remote middleware services. + + If you expose the Admin API and enable the Set Middleware API, you should: + + - Run Hoverfly only on trusted/private networks. + - Restrict access to the Admin API to trusted callers and networks (e.g., via firewalls, security groups, VPNs, reverse proxy ACLs). + - Prefer binding to localhost unless there is a strong need to expose it, and scope exposure to the minimum required interfaces. + - Enable authentication if appropriate and avoid exposing the Admin port publicly. + +The guidance above applies whether you configure middleware as a local executable/script or as HTTP middleware. From 2f5ad4542ea3446944e2211859555a60dbb2e171 Mon Sep 17 00:00:00 2001 From: Tommy Situ Date: Tue, 9 Sep 2025 23:40:52 +0100 Subject: [PATCH 5/6] Improve middleware docs --- docs/pages/keyconcepts/middleware.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/keyconcepts/middleware.rst b/docs/pages/keyconcepts/middleware.rst index 0fc68df52..ed4afb768 100644 --- a/docs/pages/keyconcepts/middleware.rst +++ b/docs/pages/keyconcepts/middleware.rst @@ -74,6 +74,6 @@ By default, Hoverfly binds its Admin and Proxy ports to the loopback interface o - Run Hoverfly only on trusted/private networks. - Restrict access to the Admin API to trusted callers and networks (e.g., via firewalls, security groups, VPNs, reverse proxy ACLs). - Prefer binding to localhost unless there is a strong need to expose it, and scope exposure to the minimum required interfaces. - - Enable authentication if appropriate and avoid exposing the Admin port publicly. + - :ref:`proxyauth` if appropriate and avoid exposing the Admin port publicly. The guidance above applies whether you configure middleware as a local executable/script or as HTTP middleware. From 24ef3527621fe9b58ac820f5e48a826287447f61 Mon Sep 17 00:00:00 2001 From: Tommy Situ Date: Wed, 10 Sep 2025 08:55:48 +0100 Subject: [PATCH 6/6] Fix flaky test in ci pipeline --- core/delay/log_normal_generator_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/delay/log_normal_generator_test.go b/core/delay/log_normal_generator_test.go index 603b5a9d5..b86eb2b7c 100644 --- a/core/delay/log_normal_generator_test.go +++ b/core/delay/log_normal_generator_test.go @@ -1,14 +1,15 @@ package delay import ( + "sort" + "testing" + . "github.com/onsi/gomega" "gonum.org/v1/gonum/floats" "gonum.org/v1/gonum/stat" - "sort" - "testing" ) -const tolerance = 10 +const tolerance = 50 func TestLogNormalGenerator_GenerateDelay(t *testing.T) { RegisterTestingT(t)