critical

GHSA-q6xx-5vr8-p898

Go · github.com/nezhahq/nezha

Summary

Nezha vulnerable to cross-tenant terminal/file-manager session hijack via WebSocket stream UUID without ownership check

Severity
critical
CVSS
9.9
CWE
CWE-639, CWE-862
Published
2026-06-26
Updated
2026-06-26

Advisory details

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:

Any authenticated user with access to one of these side channels can attach to a live session.

PoC

  1. Deploy nezha v2.0.9. Add at least one server. Configure two accounts: admin (RoleAdmin, owns the server) and member (RoleMember, no access to that server).
  2. As admin, open the web terminal for the server. The browser opens wss://<dashboard>/ws/terminal/<UUID>. Capture this UUID from the network inspector, server access log, or Referer header.
  3. From a separate session logged in as member, open wss://<dashboard>/ws/terminal/<UUID> (same UUID). The member's WebSocket attaches to the same ioStreamContext because terminalStream only checks GetStream(streamId) — no ownership check.
  4. 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

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"):

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

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.