GHSA-q6xx-5vr8-p898
CRITICALGHSA-q6xx-5vr8-p898 is a critical-severity (CVSS 9.9) remote code execution vulnerability in github.com/nezhahq/nezha. O3 Security confirms whether GHSA-q6xx-5vr8-p898 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Nezha vulnerable to cross-tenant terminal/file-manager session hijack via WebSocket stream UUID without ownership check
Real-World Exposure
github.com/nezhahq/nezha🐹github.com/nezhahq/nezhaReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
Summary
In nezha v1.14.13–v1.14.14 and v2.0.0–v2.0.9, the WebSocket endpoints GET /ws/terminal/:id and GET /ws/file/:id authenticate the caller only by the presence of a valid stream UUID, with no ownership check tying that UUID to the user who created the stream. Any authenticated dashboard user (including a RoleMember) who learns a live stream UUID can attach to the session and gain interactive shell access or full file-manager control on the target server — i.e. cross-tenant RCE.
This was silently fixed in commit 6661d6a (2026-05-18, shipped in v2.0.10). At submission time no public CVE/GHSA covers this fix, so operators of v1.14.x and pre-v2.0.10 v2.x deployments have no signal that they are running vulnerable code.
Details
Stream allocation — service/rpc/io_stream.go (v2.0.9):
func (s *NezhaHandler) CreateStream(streamId string) {
s.ioStreamMutex.Lock()
defer s.ioStreamMutex.Unlock()
s.ioStreams[streamId] = &ioStreamContext{
userIoConnectCh: make(chan struct{}),
agentIoConnectCh: make(chan struct{}),
}
}
No creator is bound to the stream.
Stream attach — cmd/dashboard/controller/terminal.go (v2.0.9):
// @Router /ws/terminal/{id} [get]
func terminalStream(c *gin.Context) (any, error) {
streamId := c.Param("id")
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
return nil, err
}
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
// ... WebSocket upgrade and bidirectional pipe ...
}
The only authorization check is GetStream(streamId) — "does this UUID exist in the in-memory map". getUid(c) is never compared against the user who called createTerminal. The same pattern is present in fmStream(c) in cmd/dashboard/controller/fm.go.
Where the UUID leaks:
createTerminal returns the UUID to the legitimate client, which then opens wss://<dashboard>/ws/terminal/<UUID>. As a URL path component the UUID is exposed via:
- Reverse-proxy access logs (nginx, Caddy, Cloudflare).
- Referer headers when the page embeds external resources or error reporters.
- Browser history / bookmark sync.
- Frontend telemetry (Sentry, Bugsnag) breadcrumbs that include the WebSocket URL.
- Any shared-tenant or multi-operator log viewer.
Any authenticated user with access to one of these side channels can attach to a live session.
PoC
- Deploy nezha v2.0.9. Add at least one server. Configure two accounts:
admin(RoleAdmin, owns the server) andmember(RoleMember, no access to that server). - As
admin, open the web terminal for the server. The browser openswss://<dashboard>/ws/terminal/<UUID>. Capture this UUID from the network inspector, server access log, orRefererheader. - From a separate session logged in as
member, openwss://<dashboard>/ws/terminal/<UUID>(same UUID). The member's WebSocket attaches to the sameioStreamContextbecauseterminalStreamonly checksGetStream(streamId)— no ownership check. - The member can now read the admin's shell output and inject keystrokes, achieving shell-level RCE on the target server, with no visible signal to the legitimate session owner.
Same flow works against /ws/file/:id (file-manager hijack: arbitrary read/write on the target server's filesystem).
Impact
- Severity: Critical. Interactive RCE on a server administered by another user, with no audit signal to the rightful session owner.
- Attack complexity: Low. The attacker needs an authenticated dashboard account (which any
RoleMemberis) and one captured UUID from a side channel. - Confidentiality / Integrity / Availability: all High.
/ws/file/:idexposes arbitrary read+write on the target filesystem;/ws/terminal/:idis a full shell.
This is the same impact tier as CVE-2026-46716 (cross-tenant cron RCE) and arguably worse, because the entry point is a passively-leaked URL rather than an authenticated POST — attackers do not need direct dashboard interaction once the UUID is leaked through logs or telemetry.
Fix reference
Already fixed in master by commit 6661d6a ("fix(rpc): bind io_stream sessions to creator to prevent terminal/fm hijack"):
CreateStreamnow accepts acreatorUserID uint64and stores it on theioStreamContext.- New
IsStreamAuthorizedForUser(streamId, userID, isAdmin)helper. terminalStreamandfmStreamcall this helper before the WebSocket upgrade and before thedefer CloseStream(streamId), so a rejected attempt does not tear down a legitimate stream.
Shipped in v2.0.10 (2026-05-19). The v1.14 line has not received a backport.
Why this advisory
The fix landed silently. The other May 17–21 fixes received public GHSAs (GHSA-99gv-2m7h-3hh9, GHSA-rxf6-wjh4-jfj6, GHSA-hvv7-hfrh-7gxj, GHSA-w4g9-mxgg-j532, GHSA-6x26-5727-rrm9, GHSA-4g6j-g789-rghm) covering cron RCE, AlertRule trigger, telemetry leak, notification SSRF, DDNS SSRF, and agent forge-results respectively — but none cover the terminal / file-manager session hijack. This advisory closes that gap so operators of v1.14.x and v2.0.0–v2.0.9 know to upgrade.
Recommended action
- Publish this GHSA so v2.x operators below v2.0.10 see the alert in their dependency scanners.
- Either backport
6661d6ato a v1.14.15 release, or mark the v1.14 line end-of-life inSECURITY.mdso operators understand the support boundary.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/nezhahq/nezha | ≥ 1.14.13 | No fix |
| 🐹Go | github.com/nezhahq/nezha | ≥ 2.0.0&&< 2.0.10 | 2.0.10 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/nezhahq/nezha. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
No patched version of github.com/nezhahq/nezha has shipped for GHSA-q6xx-5vr8-p898 yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-q6xx-5vr8-p898 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-q6xx-5vr8-p898. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-q6xx-5vr8-p898 in your dependencies?
O3 detects GHSA-q6xx-5vr8-p898 across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.