← Back to list

CVE-2025–54123 Hoverfly ≤1.11.3 Command Injection (RCE) Case Study & Patch Diffing

phantom_hat · 2026-05-27 23:02 · 50 claps · 10.3 min read
#rce #vulnerability #vulnerability-research #command-injection #code-review
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity 🥊 · Combat Sports

CVE-2025–54123 Hoverfly ≤1.11.3 Command Injection (RCE) Case Study & Patch Diffing

Introduction

This Case Study focuses on CVE-2025–54123, a critical Remote Code Execution vulnerability discovered in Hoverfly, an open-source API simulation and service virtualization tool widely used in CI/CD pipelines and development environments. The CVSS score is 9.8 CRITICAL (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), and it affects all versions ≤ 1.11.3.

The vulnerability is rooted in OS Command Injection (CWE-78) combined with Improper Input Validation (CWE-20) and exists in the middleware management API endpoint:

In this case study I will demonstrate the Target Overview, Discovery & Source Code Analysis, the three code-level flaws that combine to create the injection primitive, Manual Exploitation, and a deep Patch Diffing of the fix introduced in version 1.12.0.

NVD Reference: https://nvd.nist.gov/vuln/detail/CVE-2025-54123

GitHub Advisory: https://github.com/SpectoLabs/hoverfly/security/advisories/GHSA-r4h8-hfp2-ggmf

Target Overview

Hoverfly (by SpectoLabs) is a lightweight, open-source service virtualization tool written in Go. It allows developers and testers to simulate HTTP(S) services, capture and replay API traffic, and apply middleware transformations to request/response pairs in real time.

From a security perspective, Hoverfly exposes an Admin API (default port 8888) which provides management endpoints including:

  • GET /api/v2/hoverfly/middleware retrieve current middleware config
  • PUT /api/v2/hoverfly/middleware set middleware binary and/or script

The PUT endpoint is the attack surface. It accepts a JSON body specifying a binary field (and optionally a script field) which Hoverfly uses to configure a middleware executable. This user-supplied value is ultimately passed directly to exec.Command() with no sanitization giving an attacker the ability to execute arbitrary OS commands on the host with the privileges of the Hoverfly process.

Black Box Discovery / Analysis

Black Box Enumeration

The middleware endpoint is only accessible to authorized users so analyzing it requires proper authorization.

we have to login to interact with middleware

Login page

Login page

After logging in it redirects to /dashboard

Dashboard

Dashboard

Here are some interesting fields which are Binary and Script as name suggests it’s something related to script execution.

after logging in I noticed that it’s sending GET Request to 2 endpoints with the delay of 5 seconds

  • /api/v2/hoverfly
  • /api/v2/hoverfly/middleware

The response of /api/v2/hoverfly/middleware looks interesting because it contains a json response with 3 keys binary, script, remote

{
    "binary": "",
    "script": "",
    "remote": ""
}

By modifying request to /api/v2/hoverfly/middlewareendpoint from GET to OPTIONS the Response reveals that it supports 3 HTTP Methods GET, PUT, OPTIONS

Black Box Discovery

After Changing the request Method to PUT without json body

Looks like it require a valid json format let’s add with empty values.

with empty json it sent what input values it accepts to run binary and scripts and remote looks like it requires a remote server, let’s first try with binary and script parameters to see how it behave.

Exploitation

for the value of binary we have to give a valid binary like ruby or python since the second parameter suggest it require some kind of script so we can pass a script related to that binary

FOR EXAMPLE: if we set the value of binary to python3 since it is a binary of python3 then we can pass some python code as the value of script for example

{
    "binary":"python3",
    "script":"print(\"hi hacker\")"
}

let’s try and see what’s the behavior

it throws an error but if we look closely after STDOUT we can see there’s the output of our print statement. means we achieved code execution on the target machine now let’s try with bash since the bash binary is located at /bin/bash

Amazing here’s the output of of id command after STDOUT and we achieved RCE at the victim machine.

We achieved the RCE on the victim machine now it’s time to move on towards White box analysis and deep dive in the depth of this vulnerability and differentiate between patched code and vulnerable code.

White Box Discovery / Analysis

Architecture of the Middleware System

Hoverfly’s middleware feature is designed to allow custom transformation of request/response pairs. The flow is:

Admin API PUT /api/v2/hoverfly/middleware
        ↓
hoverfly_service.go  →  SetMiddleware()
        ↓
middleware.go  →  ConvertToNewMiddleware() / SetBinary()
        ↓
local_middleware.go  →  executeMiddlewareLocally()  →  exec.Command(this.Binary)

Three separate files each contribute one flaw. Together, they create a fully weaponizable RCE chain.

Hoverfly Middleware request handling architecture

Hoverfly use it’s own handler package to handle requests it has functions for reading the requests and writing the response *ReadFromRequest*(***request*** *http.Request, ***v*** interface{}) , *writeResponse*(***response*** http.ResponseWriter, ***bytes*** []byte, ***contentType*** string) and others like for writing error response or write response with content type.

then this package is used in the file hoverfly-1.11.3/core/handlers/v2/hoverfly_middleware_handler.go and this file is used to handle the requests of middleware. this file has

  • 1 Interface named HoverflyMiddleware
  • 1 Struct named HoverflyMiddlewareHandler
  • 4 Functions
  1. *RegisterRoutes*(***mux*** *bone.Mux, ***am*** *handlers.AuthHandler) for registering routes.
  2. *Get*(***w*** http.ResponseWriter, ***req*** *http.Request, ***next*** http.HandlerFunc) to handle GET Requests for middleware
  3. *Put*(***w*** http.ResponseWriter, ***req*** *http.Request, ***next*** http.HandlerFunc)to handle PUT Requests for middleware
  4. *Options*(***w*** http.ResponseWriter, ***r*** *http.Request, ***next*** http.HandlerFunc) to handle OPTIONS Requests for middleware
func (this *HoverflyMiddlewareHandler) RegisterRoutes(mux *bone.Mux, am *handlers.AuthHandler) {
 mux.Get("/api/v2/hoverfly/middleware", negroni.New(
  negroni.HandlerFunc(am.RequireTokenAuthentication),
  negroni.HandlerFunc(this.Get),
 ))

 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),
 ))
}

This function register routes and we can see if we send GET or PUT Requests to middleware endpoint /api/v2/hoverfly/middleware it checks the token authentication using the function of AuthHandler struct in from the package handlers the function name is RequireTokenAuthentication which is available in the file hoverfly-1.11.3/core/handlers/auth_handler.go

We can send json payload using PUT request so let’s analyze how it handles our request

in other functions like Options and Get the work is simple in Get Function it just send the response with the values of Binary Script and Remote from the struct

middlewareView.Binary, middlewareView.Script, middlewareView.Remote = this.Hoverfly.GetMiddleware()
// SNIP
handlers.WriteResponse(w, middlewareBytes)

and The Option function only send the header by setting the values using Add function from http library this line sets the Allow Header value to OPTIONS, GET, PUT

w.Header().Add("Allow", "OPTIONS, GET, PUT")

Tracing Put Request Flow

Now the input is flowing from the Put Function

func (this *HoverflyMiddlewareHandler) Put(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
 var middlewareReq MiddlewareView
 err := handlers.ReadFromRequest(req, &middlewareReq)
 if err != nil {
  handlers.WriteErrorResponse(w, err.Error(), 400)
  return
 }

 err = this.Hoverfly.SetMiddleware(middlewareReq.Binary, middlewareReq.Script, middlewareReq.Remote)
 if err != nil {
  handlers.WriteErrorResponse(w, err.Error(), 422)
  return
 }

 this.Get(w, req, next)
}

1st line initializes a struct variable named middlewareReq of struct MiddlewareView

type MiddlewareView struct {
 Binary string `json:"binary"`
 Script string `json:"script"`
 Remote string `json:"remote"`
}

The MiddlewareView struct defines the expected request data model containing three string fields Binary, Script, and Remote each mapped to corresponding JSON keys

then it reads the request from it’s own reader from package handlers

this.Hoverfly.*SetMiddleware*(middlewareReq.Binary, middlewareReq.Script, middlewareReq.Remote)

this line sets the middleware let’s break it down

HoverflyMiddlewareHandler this is the struct in this file which is referenced as this and it has an Interface as Hoverfly

type HoverflyMiddleware interface {
 GetMiddleware() (string, string, string)
 SetMiddleware(string, string, string) error
}

type HoverflyMiddlewareHandler struct {
 Hoverfly HoverflyMiddleware
}

and if error occurs it will use WriteErrorResponse to send the error response with status code 422

Now our input values are going towards *SetMiddleware *Function

Malformed JSON Error

let’s analyze the function ReadFromRequest to see why we get the Malformed JSON Error when we sent the empty request in our previous Black Box testing

body, _ := io.ReadAll(request.Body)

 err := json.Unmarshal(body, &v)
 if err != nil {
  return errors.New("Malformed JSON")
 }

these lines are critical to understand why we got that error before

First it reads the Request body using http.Request.Body and read that body using the ReadAll function from io library and set it to the variable named body

The json.Unmarshal(body, &v) line parses the JSON data from the request body and stores the decoded values inside the object referenced by v, which is passed dynamically through the empty interface (interface{}). but in the previous scenario we didn’t sent any json data so the Unmarshal trows and error and then if condition became true and it throws the error.

SetMiddleware Mechanism

func (hf *Hoverfly) SetMiddleware(binary, script, remote string) error

This is the function definition

newMiddleware := &middleware.Middleware{}
 if binary == "" && script == "" && remote == "" {
  hf.Cfg.Middleware = *newMiddleware
  return nil
 }

then it initializes a struct variable named newMiddleware of the struct Middleware without any values which is clearing the values of middleware this struct is available at hoverfly package middleware in the file hoverfly-1.11.3/core/middleware/middleware.go Here’s the struct definition:

type Middleware struct {
 Binary string
 Script *os.File
 Remote string
}

now from the Middleware struct these functions will set the values of our input values

if binary == "" && script != "" {
  return fmt.Errorf("cannot run script with no binary")
 }

 err := newMiddleware.SetBinary(binary)
 if err != nil {
  return err
 }

 err = newMiddleware.SetScript(script)
 if err != nil {
  return err
 }

 err = newMiddleware.SetRemote(remote)
 if err != nil {
  return err
 }

SetBinary and SetRemote functions are simple setter functions

func (this *Middleware) SetBinary(binary string) error {
 this.Binary = binary
 return nil
}

func (this *Middleware) SetRemote(remoteUrl string) error {
 this.Remote = remoteUrl
 return nil
}

these function sets the provided value to the struct.

SetScript Key Behavior Analysis:

func (this *Middleware) SetScript(scriptContent string) error {
  • This function takes script content as input and attaches it to the middleware system in a controlled way.
  • Instead of storing raw input directly, it creates a temporary file-based representation.

1. Temporary Workspace Creation

tempDir := path.Join(os.TempDir(), "hoverfly")
os.Mkdir(tempDir, 0777)
  • A dedicated temporary directory is prepared inside the system temp folder.
  • This ensures scripts are stored in an isolated environment.
  • Even if the directory already exists, errors are ignored intentionally.

2. Script File Creation (Core Mechanism)

newScript, err := os.CreateTemp(tempDir, "hoverfly_")
  • A unique temporary file is created for the script.
  • Each execution gets a separate file to avoid overwriting or conflicts.

3. Writing User Input to File

_, err = newScript.Write([]byte(scriptContent))
  • The actual script content provided by the user is written into the temporary file.
  • At this point, the middleware is no longer holding raw input it is persisted on disk.

4. Replacing Old Script Safely

this.DeleteScript()
this.Script = newScript
  • Any previously stored script is deleted first.
  • Then the new script file is assigned to the middleware.

in the SetMiddleware after setting all values using setter functions it prepares the test request data

testData := models.RequestResponsePair{
  Request: models.RequestDetails{
   Path:        "/",
   Method:      "GET",
   Destination: "www.test.com",
   Scheme:      "",
   Query:       map[string][]string{},
   Body:        "",
   Headers:     map[string][]string{"test_header": {"true"}},
  },
  Response: models.ResponseDetails{
   Status:  200,
   Body:    "ok",
   Headers: map[string][]string{"test_header": {"true"}},
  },
 }

and exectution goes to Middleware Struct’s Execute Function in the middleware package

_, err = newMiddleware.Execute(testData)
 if err != nil {
  return err
 }

Middleware Execute

In this function there’s a check for Remote value of the Struct to execute locally or remotely

if this.Remote == "" {
  return this.executeMiddlewareLocally(pair)
 } else {
  return this.executeMiddlewareRemotely(pair)
 }

then Execution goes to executeMiddlewareLocally

The vulnerable Sink

In this file hoverfly-1.11.3/core/middleware/local_middleware.go we got the function executeMiddlewareLocally which has the vulnerable sink which allows user to execute commands

var middlewareCommand *exec.Cmd
 if this.Script == nil {
  middlewareCommand = exec.Command(this.Binary)
 } else {
  middlewareCommand = exec.Command(this.Binary, this.Script.Name())
 }

At the beginning of the function here’s the vulnerable sink. this.Binary and this.Script which is controlled by attacker as we observed in setter functions, that values are directly passing without any input sanatization

exec.*Command *this function returns the struct with arguments to run the command

if err := middlewareCommand.Start(); err != nil {
  log.WithFields(log.Fields{
   "sdtdout": string(stdout.Bytes()),
   "sdtderr": string(stderr.Bytes()),
   "error":   err.Error(),
  }).Error("Middleware failed to start")
  return pair, &MiddlewareError{
   OriginalError: err,
   Message:       "Middleware failed to start",
   Command:       this.toString(),
   Stdin:         string(pairViewBytes),
   Stdout:        string(stdout.Bytes()),
   Stderr:        string(stderr.Bytes()),
  }
 }

and the Start() function starts the command execution.

The Output Processing why we get result with error

Output format mismatch

Later code expects JSON:

json.Unmarshal(stdout.Bytes(), &newPairView)

If middleware returns:

uid=0(root) gid=0(root)

JSON Parsing Failure (Hidden Major Error Source)

err = json.Unmarshal(stdout.Bytes(), &newPairView)

If output is not valid JSON:

Flow becomes:

return pair, &MiddlewareError{
    Message: "Failed to unmarshal JSON from middleware",
}

This is the most common reason:

“command executed but still error returned”

Chain of errors

now the *executeMiddlewareLocally *throws an error then Execute also throws an error which leads SetMiddleware to throw error and in last the Put function in handles the error

err = this.Hoverfly.SetMiddleware(middlewareReq.Binary, middlewareReq.Script, middlewareReq.Remote)
 if err != nil {
  handlers.WriteErrorResponse(w, err.Error(), 422)
  return
 }

Output with Errors

That’s the reason we see output of our executed command with errors

for this payload

{
    "binary":"/bin/bash",
    "script":"id"
}

we got this response

{
    "error": "Failed to unmarshal JSON from middleware\nCommand: /bin/bash /tmp/hoverfly/hoverfly_3375714724\ninvalid character 'u' looking for beginning of value\n\nSTDIN:\n{\"response\":{\"status\":200,\"body\":\"ok\",\"encodedBody\":false,\"headers\":{\"test_header\":[\"true\"]}},\"request\":{\"path\":\"/\",\"method\":\"GET\",\"destination\":\"www.test.com\",\"scheme\":\"\",\"query\":\"\",\"formData\":null,\"body\":\"\",\"headers\":{\"test_header\":[\"true\"]}}}\n\nSTDOUT:\nuid=0(root) gid=0(root) groups=0(root)\n"
}

and after STDOUT: we got the output of our command

Patch Analysis

In the patch the developers disabled middleware API by default and applied that patch to related functionalities

type HoverflyMiddlewareHandler struct {
 Hoverfly HoverflyMiddleware
 Enabled  bool
}

added Enabled in HoverflyMiddlewareHandler with bool data type to enable and disable this functionality in file core/handlers/v2/hoverfly_middleware_handler.go

func (this *HoverflyMiddlewareHandler) RegisterRoutes(mux *bone.Mux, am *handlers.AuthHandler) {
 mux.Get("/api/v2/hoverfly/middleware", negroni.New(
  negroni.HandlerFunc(am.RequireTokenAuthentication),
  negroni.HandlerFunc(this.Get),
 ))

 /* mux.Put("/api/v2/hoverfly/middleware", negroni.New(
  negroni.HandlerFunc(am.RequireTokenAuthentication),
  negroni.HandlerFunc(this.Put),
 )) this is remove */
 if this.Enabled {
  mux.Put("/api/v2/hoverfly/middleware", negroni.New(
   negroni.HandlerFunc(am.RequireTokenAuthentication),
   negroni.HandlerFunc(this.Put),
  ))
 }

they added a condition to Put Function if the the Enabled is true then the Put function can be used

func (this *HoverflyMiddlewareHandler) Options(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// w.Header().Add("Allow", "OPTIONS, GET, PUT") removed
 allow := "OPTIONS, GET"
 if this.Enabled { // added
  allow += ", PUT"
 }
 w.Header().Add("Allow", allow)
 handlers.WriteResponse(w, []byte(""))
}

if enabled then append the PUT method in the allow header

the developers added the flag -enable-middleware-api to enable the middleware API

Exploit

Here’s a fully functional Exploit script in python which is available on my github repo

[embed]GitHub - 0x00phantom-hat/Hoverfly-1.11.3-RCE-CVE-2025-54123-Exploit Contribute to 0x00phantom-hat/Hoverfly-1.11.3-RCE-CVE-2025-54123-Exploit development by creating an account on GitHub.github.com

Command Executed Successfully

Conclusion

CVE-2025–54123 demonstrates how three small oversights — blind input storage, an unguarded exec.Command() sink, and no API-level validation — chain into a CVSS 9.8 RCE. The "error-as-oracle" behavior makes it worse: the command executes and leaks output even when the request technically fails.

The patch disables the PUT endpoint by default, which is a reasonable stopgap, but proper input validation and binary allowlisting are the real fix. The admin API should never be exposed to untrusted networks regardless.

user input should never reach exec.Command() without sanitization, and error messages should never echo command output.

References

[embed]NVD Hoverfly is an open source API simulation tool. In versions 1.11.3 and prior, the middleware functionality in Hoverfly…nvd.nist.gov

[embed]GitHub - SpectoLabs/hoverfly: Lightweight service virtualization/ API simulation / API mocking tool… Lightweight service virtualization/ API simulation / API mocking tool for developers and testers - SpectoLabs/hoverflygithub.com


메타데이터
post_id
aacc092f7f3a
slug
cve-2025-54123-hoverfly-1-11-3-command-injection-rce-case-study-patch-diffing-aacc092f7f3a
url
https://medium.com/@phantom_hat/cve-2025-54123-hoverfly-1-11-3-command-injection-rce-case-study-patch-diffing-aacc092f7f3a
canonical_url
https://medium.com/@phantom_hat/cve-2025-54123-hoverfly-1-11-3-command-injection-rce-case-study-patch-diffing-aacc092f7f3a
author_url
https://medium.com/@phantom_hat
status
ok
fetched_at
2026-06-13 07:35:29