Nezha vulnerable to cross-tenant terminal/file-manager session hijack via WebSocket stream UUID without ownership check
Date: 2026-06
Affected: go/github.com/nezhahq/nezha, go/github.com/nezhahq/nezha
Attack vector: ssrf
Category: vulnerability-disclosure
Severity: Critical
CWE: CWE-639
CWE-862
CVSS: 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)

### 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`](https://github.com/nezhahq/nezha/commit/6661d6a7fc1c269f55c7f4e775082ad23fbe0f54) (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):** ```go 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):** ```go // @Router /ws/terminal/{id} [get] func terminalStream(c *gin.Context) (any, error) { streamId := c.Param("id") if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {


Mitigations:

Architectural layers (MAESTRO):