Skip to content

Commit 4be8340

Browse files
tommysituclaude
andcommitted
fix(action): timeout remote post-serve HTTP client (GHSA-42j2-w334-qxw7)
Remote post-serve actions were issued through http.DefaultClient, which has no timeout. A non-responsive endpoint (accepts TCP but never replies, half-open TLS, zero-window stall) would block the calling goroutine indefinitely. Because each matched request spawns a fresh goroutine with no cap or recovery, an attacker with admin-API access could exhaust memory by pointing a registered remote action at a black-hole URL. Use a dedicated *http.Client with a 30s Timeout so every in-flight remote-action goroutine is guaranteed to terminate. The timeout is exposed as a package var (RemoteActionTimeout) so tests can lower it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent cb576ef commit 4be8340

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

core/action/action.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import (
1818
log "github.com/sirupsen/logrus"
1919
)
2020

21+
// RemoteActionTimeout caps the total time spent on a remote post-serve action
22+
// HTTP call (dial, TLS handshake, request write, response read). Without it a
23+
// non-responsive endpoint would leak the calling goroutine forever
24+
// (GHSA-42j2-w334-qxw7). Exported so tests can lower it.
25+
var RemoteActionTimeout = 30 * time.Second
26+
2127
type Action struct {
2228
Binary string
2329
Script *os.File
@@ -140,7 +146,8 @@ func (action *Action) Execute(pair *models.RequestResponsePair, journalIDChannel
140146
req.Header.Add("Content-Type", "application/json")
141147
req.Header.Add("X-CORRELATION-ID", correlationID)
142148

143-
resp, err := http.DefaultClient.Do(req)
149+
client := &http.Client{Timeout: RemoteActionTimeout}
150+
resp, err := client.Do(req)
144151
completionTime := time.Now()
145152
journalID, received := receiveJournalIdWithTimeout(journalIDChannel, time.Second)
146153
log.Info("Journal ID received ", journalID)

core/action/action_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55
"net/http/httptest"
66
"testing"
7+
"time"
78

89
"github.com/SpectoLabs/hoverfly/core/journal"
910
"github.com/gorilla/mux"
@@ -166,3 +167,44 @@ func Test_ExecuteRemotePostServeAction_WithUnReachableHost(t *testing.T) {
166167
func processHandlerOkay(w http.ResponseWriter, r *http.Request) {
167168
w.WriteHeader(200)
168169
}
170+
171+
// Regression test for GHSA-42j2-w334-qxw7: a remote post-serve action pointed
172+
// at a non-responsive endpoint must not block forever. Without the client
173+
// timeout the goroutine would leak indefinitely.
174+
func Test_ExecuteRemotePostServeAction_TimesOutOnSlowEndpoint(t *testing.T) {
175+
RegisterTestingT(t)
176+
177+
block := make(chan struct{})
178+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
179+
<-block
180+
}))
181+
defer server.Close() // runs last; needs handler to have exited
182+
defer close(block) // runs first; unblocks handler so server can close
183+
184+
originalTimeout := action.RemoteActionTimeout
185+
action.RemoteActionTimeout = 100 * time.Millisecond
186+
defer func() { action.RemoteActionTimeout = originalTimeout }()
187+
188+
newAction, err := action.NewRemoteAction(server.URL+"/slow", 0)
189+
Expect(err).To(BeNil())
190+
191+
originalPair := models.RequestResponsePair{
192+
Response: models.ResponseDetails{Body: "Normal body"},
193+
}
194+
journalIDChannel := make(chan string, 1)
195+
journalIDChannel <- "1"
196+
newJournal := journal.NewJournal()
197+
198+
done := make(chan error, 1)
199+
go func() {
200+
done <- newAction.Execute(&originalPair, journalIDChannel, newJournal)
201+
}()
202+
203+
select {
204+
case err := <-done:
205+
Expect(err).NotTo(BeNil())
206+
case <-time.After(5 * time.Second):
207+
t.Fatal("Execute did not return within 5s — timeout not applied")
208+
}
209+
close(journalIDChannel)
210+
}

0 commit comments

Comments
 (0)