CVE-2026-53522 — nezha
MEDIUMCVE-2026-53522 is a medium-severity (CVSS 6.5) CWE-770 vulnerability in github.com/nezhahq/nezha. A fix is available for github.com/nezhahq/nezha — see the affected versions and patch details below.
Nezha Monitoring: Unbounded WebSocket Streams — Resource Exhaustion DoS
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-53522.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
CVE-2026-53522 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 378,156 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
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
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 io_stream.go:59-67). There is no per-user rate limit, no global semaphore, and no per-server connection cap. Each stream allocates:
- A
ioStreamContextstruct with several channels and sync primitives - Two goroutines via
StartStream()(io_stream.go:358-369) — bidirectionalio.CopyBuffer - A gRPC IOStream between the dashboard and the agent
- An agent-side PTY/shell process
Vulnerable code:
terminal.go:27-67 — createTerminal:
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:
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
}
io_stream.go:55-67 — CreateStreamWithPurpose (inserts into unbounded map):
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{}),
}
}
io_stream.go:319-372 — StartStream spawns two goroutines per stream:
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 '{"server_id": 1}' &
done
wait
Each request:
- Creates a new stream entry in
ioStreams - Sends a
TaskTypeTerminalGRPCtask 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:
- Dashboard memory/goroutines — each stream adds goroutines, channels, and buffers
- Agent resources — each stream spawns a PTY/shell process on the monitored server
- 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:
-
Per-user stream cap in
CreateStream— reject if the user already has N active streams (e.g., 10 per user):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 ... } -
Per-server semaphore — limit concurrent streams to any single server (e.g., 20 per server)
-
Rate limiter on
createTerminalandcreateFM— mirror the existing MCP rate limiter (mcp_ratelimit.go) for legacy WebSocket endpoints -
Add a configurable
MaxStreamsPerUser/MaxStreamsPerServersetting so operators can tune limits without code changes
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/nezhahq/nezha | ≥ 1.0.0&&< 2.2.0 | 2.2.0go get github.com/nezhahq/nezha@v2.2.0 |
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, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update github.com/nezhahq/nezha to 2.2.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-53522 is resolved across your whole dependency graph.
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 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-53522 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-53522. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-53522 in your dependencies?
O3 Security finds CVE-2026-53522 across Go dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.