CVE-2026-53522: Nezha Monitoring: Unbounded WebSocket Streams — Resource Exhaustion DoS
1. Description
The Nezha dashboard exposes two endpoints that create long-lived WebSocket streams to monitored agents:
- POST /api/v1/terminal → createTerminal() (terminal.go:27-67) - POST /api/v1/file → createFM() (fm.go:28-67)
Both call rpc.NezhaHandlerSingleton.CreateStream(streamId, ...) which inserts a new ioStreamContext into an unbounded map[string]ioStreamContext (s.ioStreams in iostream.go:59-67). There is no per-user rate limit, no global semaphore, and no per-server connection cap. Each stream allocates:
1. A ioStreamContext struct with several channels and sync primitives 2. Two goroutines via StartStream() (iostream.go:358-369) — bidirectional io.CopyBuffer 3. A gRPC IOStream between the dashboard and the agent 4. An agent-side PTY/shell process
Vulnerable code:
terminal.go:27-67 — createTerminal: go func createTerminal(c gin.Context) (model.CreateTerminalResponse, error) { // ... validation ... rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID) // ... sends TaskTypeTerminalGRPC to agent ... return &model.CreateTerminalResponse{...}, nil }
fm.go:28-67 — createFM: go func createFM(c gin.Context) (model.CreateFMResponse, error) { // ... validation ... rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID) // ... sends TaskTypeFM to agent ... return &model.CreateFMResponse{...}, nil }
iostream.go:55-67 — CreateStreamWithPurpose (inserts into unbounded map): go func (s NezhaHandler) CreateStreamWithPurpose(...) { s.ioStreamMutex.Lock() defer s.ioStreamMutex.Unlock() s.ioStreams[streamId] = &ioStreamContext{ creatorUserID: creatorUserID, targetServerID: targetServerID, purpose: purpose, userIoConnectCh: make(chan struct{}), agentIoConnectCh: make(chan struct{}), revokedCh: make(chan struct{}), } }
iostream.go:319-372 — StartStream spawns two goroutines per stream: go func (s NezhaHandler) StartStream(streamId string, timeout time.Duration) error { // ... go func() { , innerErr := io.CopyBuffer(userIo, agentIo, bp.buf) errCh <- innerErr }() go func() { , innerErr := io.CopyBuffer(agentIo, userIo, bp.buf) errCh <- innerErr }() return <-errCh }
The NezhaHandler.ioStreams map is initialized as a plain make(map[string]ioStreamContext) in nezha.go:36 — no capacity limit, no eviction policy beyond explicit CloseStream / RevokeStreamsForServer.
The HasPermission check at terminal.go:41-43 and fm.go:43-45 controls access scope but does not limit creation volume. A user with ScopeServerExec (terminal) or ScopeServerRead+Write+Delete (file manager) can open unlimited streams.
2. PoC
A conceptual attack (no Docker needed):
As an authenticated user with a valid JWT or PAT: for i in {1..1000}; do curl -X POST "https://dashboard.example.com/api/v1/terminal" \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{"serverid": 1}' & done wait
Each request: - Creates a new stream entry in ioStreams - Sends a TaskTypeTerminalGRPC task to the agent - When the WebSocket attachment occurs (GET /ws/terminal/{id}), spawns 2 goroutines for I/O relay and allocates a 1 MB buffer per goroutine
The attack targets three resource domains: 1. Dashboard memory/goroutines — each stream adds goroutines, channels, and buffers 2. Agent resources — each stream spawns a PTY/shell process on the monitored server 3. gRPC connection pool — concurrent IOStreams consume gRPC multiplexing capacity
The POST /file (createFM) endpoint provides an alternative path with the same unbounded behavior, using ScopeServerRead+Write+Delete instead of ScopeServerExec.
3. Impact
- Denial of Service against the dashboard: memory exhaustion, goroutine starvation, or gRPC stream table overflow from rapid stream creation - Denial of Service against monitored agents: each terminal session spawns a PTY process on the agent — an attacker can crash or degrade all agents behind the dashboard - Operational cascade: if the dashboard OOMs, all agent monitoring and alerting is lost - PAT connection-registry bypass: rapid create-connect-disconnect cycles may evade cleanup tracking
The attack requires only authenticated access with standard scopes — no special privileges. Any team member with terminal access to a server can DoS the entire infrastructure.
4. Remediation
Implement layered rate limiting and concurrency control:
1. Per-user stream cap in CreateStream — reject if the user already has N active streams (e.g., 10 per user): go func (s NezhaHandler) CreateStreamWithPurpose(...) { s.ioStreamMutex.Lock() defer s.ioStreamMutex.Unlock() count := 0 for , ctx := range s.ioStreams { if ctx.creatorUserID == creatorUserID { count++ } } if count >= maxStreamsPerUser { return error } // ... existing code ... }
2. Per-server semaphore — limit concurrent streams to any single server (e.g., 20 per server)
3. Rate limiter on createTerminal and createFM — mirror the existing MCP rate limiter (mcpratelimit.go) for legacy WebSocket endpoints
4. Add a configurable MaxStreamsPerUser / MaxStreamsPerServer setting so operators can tune limits without code changes
Other sources
Nezha Monitoring is a self-hostable, lightweight, servers and websites monitoring and O&M tool. From version 1.0.0 to before version 2.2.0, the Nezha dashboard exposes two endpoints that create long-lived WebSocket streams to monitored agents: POST /api/v1/terminal → createTerminal() (terminal.go:27-67) and POST /api/v1/file → createFM() (fm.go:28-67). Both call rpc.NezhaHandlerSingleton.CreateStream(streamId, ...) which inserts a new ioStreamContext into an unbounded map[string]ioStreamContext (s.ioStreams in iostream.go:59-67). There is no per-user rate limit, no global semaphore, and no per-server connection cap. This issue has been patched in version 2.2.0.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/nezhahq/nezhato a version that resolves this vulnerability.Fixed in 2.2.0 - Upgrade
Upgrade
Nezha dashboardto a version that resolves this vulnerability.Fixed in 2.2.0 - Configuration
Implement/enable a per-user stream cap in CreateStream: track active ioStreams by creatorUserID and reject CreateStream/CreateStreamWithPurpose when the user has reached MaxStreamsPerUser (e.g., 10 per user).
Nezha dashboard (io_stream / stream creation) MaxStreamsPerUser = configurable (apply per-user active stream cap; reject in CreateStream when user already has N active streams) - Configuration
Implement/enable a per-server concurrency control: limit concurrent ioStreams/active streams to MaxStreamsPerServer for any single targetServerID using a per-server semaphore.
Nezha dashboard (io_stream / stream creation) MaxStreamsPerServer = configurable (apply semaphore/limit per monitored server; e.g., 20 per server) - Configuration
Add a rate limiter to POST /api/v1/terminal (createTerminal), mirroring the existing MCP rate limiter behavior (mcp_ratelimit.go) used for legacy WebSocket endpoints, so rapid createTerminal calls are throttled.
Nezha dashboard (terminal.go) rate limiter for createTerminal = enabled - Configuration
Add a rate limiter to POST /api/v1/file (createFM), mirroring the existing MCP rate limiter behavior (mcp_ratelimit.go) used for legacy WebSocket endpoints, so rapid createFM calls are throttled.
Nezha dashboard (fm.go) rate limiter for createFM = enabled - Configuration
Change ioStreams from an unbounded map (initialized via make(map[string]*ioStreamContext)) to a bounded structure with an explicit capacity limit and eviction/cleanup behavior tied to CloseStream/RevokeStreamsForServer, to prevent resource exhaustion from unlimited stream entries.
Nezha dashboard (io_stream.go) s.ioStreams map capacity/eviction policy = bounded
Event History
Frequently Asked Questions
What is the severity of CVE-2026-53522?
The severity of CVE-2026-53522 is rated as medium with a score of 6.5.
What are the potential impacts of CVE-2026-53522?
CVE-2026-53522 can lead to resource exhaustion, resulting in a Denial of Service (DoS) for the Nezha Monitoring application.
How do I fix CVE-2026-53522?
To fix CVE-2026-53522, update to Nezha Monitoring version 2.2.0 or later, which mitigates the WebSocket stream vulnerability.
Which versions of Nezha Monitoring are affected by CVE-2026-53522?
CVE-2026-53522 affects Nezha Monitoring versions from 1.0.0 to before 2.2.0.
What type of vulnerability is CVE-2026-53522?
CVE-2026-53522 is classified as an unbounded WebSocket stream vulnerability that can cause resource exhaustion.