GitHub Advisoryadvisory · Go
GHSA-4fwh-wrm6-97xm Klever-Go: Unauthenticated WebSocket /subscribe: no read-size limit, no connection cap, permissive origin -> remote node memory/goroutine exhaustion (DoS)
HIGH
Official page ↗
- Aliases
- CVE-2026-86065
- Ecosystem
- Go
- Packages
- github.com/klever-io/klever-go
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-770
- Reviewed
- 2026-09-23
Affected packages
• Go github.com/klever-io/klever-go: < 1.7.20 → fixed in 1.7.20
Description
Summary
The unauthenticated WebSocket endpoint `GET /subscribe` is registered `open: true` by default
(`config/node/api.yaml`) and lets a remote, unauthenticated client exhaust the node's memory and
goroutines. Because the REST API runs IN-PROCESS with the node — `network/api/api.go` `Start(...)`
ends with `ws.Run(kleverFacade.RestAPIInterface())` — exhausting/killing the API process takes down
the entire node, including its P2P and consensus participation. No API key, account, stake, or funds
are required.
Three compounding, independently-exploitable gaps stack on this one endpoint:
1. Permissive origin — `upgrader.CheckOrigin` always returns `true`
(`network/api/websocket/routes.go`), so any web origin can complete the handshake.
2. No read-size limit — the connection never calls `conn.SetReadLimit(...)`. gorilla's default is
UNLIMITED, so a single `conn.ReadJSON` (`processSubscription`) or `conn.ReadMessage`
(`client.loopIn`) can be forced to allocate an arbitrarily large buffer from ONE frame.
3. No connection / fan-out cap — the gin global throttler (`simultaneousRequests: 100`) releases its
slot as soon as `handleSubscribe` returns, which it does immediately after
`go processSubscription(conn, hub)`. Live WebSocket connections are therefore NOT counted by it.
There is no per-IP / per-connection / hub-level cap. Each accepted connection spawns 2 goroutines
plus a 500-entry buffered channel, and `req.Addresses` has no length cap, so the hub's
`addressSubscription` map grows 1:1 with attacker-supplied strings.
Affected Component / Code Path
Unauthenticated, reachable by default, no recovery on the resource-allocation path:
```
gin engine (network/api/api.go: Start -> ws.Run, IN-PROCESS with node)
-> GET /subscribe network/api/websocket/routes.go:34 (SubscribeTopics)
-> handleSubscribe network/api/websocket/routes.go:39
-> upgrader.Upgrade (CheckOrigin == true) network/api/websocket/routes.go:22 go processSubscription(conn, hub) network/api/websocket/routes.go:46 (throttler slot freed here)
-> conn.ReadJSON(&req) (no SetReadLimit) network/api/websocket/routes.go:57 hub.HandleClientInsertion(...) websocket/websocket.go:121 websocket.NewClient -> loopIn/loopOut (2 goroutines + 500-buf chan per conn) websocket/client.go:24
-> conn.ReadMessage() (no SetReadLimit, no deadline) websocket/client.go:77 gin global throttler slot released (GAP #3)
}
func processSubscription(conn *gorilla.Conn, hub *websocket.SocketHub) {
// no conn.SetReadLimit(...) anywhere (GAP #2)
_ = conn.SetReadDeadline(time.Now().Add(subscribeReadTimeout))
var req subscribeRequest
if err := conn.ReadJSON(&req); err != nil { ... } // unbounded read
_ = conn.SetReadDeadline(time.Time{}) // deadline cleared
...
client := websocket.NewClient(conn, hub)
hub.HandleClientInsertion(parsedTypes, req.Addresses, client) // req.Addresses uncapped (GAP #3)
}
```
`websocket/websocket.go` — `HandleClientInsertion` inserts every address with no length cap:
```go
for _, address := range addresses {
if _, ok := h.addressSubscription[address]; !ok {
h.addressSubscription[address] = make(map[*client]userOptions) // grows 1:1 with attacker input
}
...
}
```
`websocket/client.go` — `loopIn` reads with no size limit and no deadline:
```go
for {
messageType, message, err := c.conn.ReadMessage() // GAP #2: unbounded, no SetReadLimit
...
}
```
Preconditions
• The node's REST API must be reachable by the attacker. Two realistic deployment shapes:
• (a) Operator-exposed API — `--rest-api-interface :8080` / `0.0.0.0:8080`. This is the standard
configuration for public RPC and observer infrastructure (the kind Klever itself operates at
`node.klever.org` / `api.klever.org`). Here the attacker reaches `/subscribe` directly over the
network with no further conditions.
• (b) Cross-origin browser drive-by — default bind is `localhost:8080`
(`common/facade/nodeFacade.go` `DefaultRestInterface = "localhost:8080"`). Because
`CheckOrigin` returns `true` (GAP #1), any website an operator visits can open
`ws://localhost:8080/subscribe` from the victim's browser and drive GAP #2 (single oversized
frame) and GAP #3 (many connections) without the API being network-exposed at all.
• `/subscribe` is `open: true` in the default `config/node/api.yaml`; `isSubscriptionRouteEnabled`
returns true and the route + hub are wired unconditionally in `RegisterRoutes`.
• `/subscribe` is NOT listed in `endpointsThrottlers` (`config/node/config.yaml`), so it has no
per-endpoint goroutine cap.
• No authentication, no on-chain account, no stake, no attacker-created asset is required.
Impact (distributed by gap and by blast radius)
This single finding produces several distinct impacts because the three gaps amplify different
node resources and reach the node through two different exposure models. They are broken out so the
remediation owner can scope each one.
Impact A — Single-frame heap exhaustion (GAP #2, the cleanest primitive)
• One unauthenticated connection sends ONE WebSocket frame; with no `SetReadLimit`, gorilla buffers
the entire frame in memory before the JSON is even parsed. Frame size scales the allocation
linearly, so one connection can drive a multi-GB allocation.
• Observed amplification: an 8 MiB frame grows the server heap by ~32 MiB (~4x) while buffering ONE
attacker frame (decode/UTF-8/scratch overhead on top of the raw bytes).
• No flood and no rate-limit interaction is needed: the source throttler is a per-IP RATE cap on
HTTP handshakes, not a size or memory cap, so a single slow connection streaming one oversized
message is not meaningfully throttled.
• Result: OOM-kill of the node process from a single connection.
Impact B — Connection / goroutine exhaustion (GAP #3, fan-out)
• Live WS connections are not counted by the gin global throttler (its slot is freed at the
HTTP→WS upgrade), and there is no per-IP or hub-level connection cap.
• Each accepted connection costs 2 goroutines + a 500-entry buffered channel. Connection count grows
linearly with attacker effort from a single source, with no ceiling.
• Result: goroutine/descriptor/scheduler exhaustion → node slowdown then OOM/crash.
Impact C — Unbounded subscription-map growth (GAP #3, per-connection memory)
• `req.Addresses` is uncapped, and `HandleClientInsertion` inserts every entry into the hub's
`addressSubscription` map. ONE connection submitting N attacker-controlled address strings grows
the map to exactly N entries (1:1), independent of how many real on-chain addresses exist.
• Result: heap growth driven purely by attacker-chosen strings on a single connection; combinable
with Impact B (many connections × many addresses) for multiplicative memory pressure.
Impact D — Cross-origin reach to localhost-bound nodes (GAP #1, exposure amplifier)
• Because `CheckOrigin` is always `true`, Impacts A–C are reachable from a victim's browser even
when the API is bound to `localhost` and never exposed to the network. A node operator who simply
visits a malicious page can have their own node driven into Impact A/B/C from inside their browser.
• Result: the `localhost`-bind "mitigation" does not hold against a web-drive-by attacker.
Blast-radius note (…
Related
Alias · CVE (1)
- CVE-2026-86065 Klever-Go is the Go implementation of the Klever blockchain protocol. vulnerability
Weakness · CWE (1)
- CWE-770 Allocation of Resources Without Limits or Throttling weakness
References
- https://github.com/klever-io/klever-go/security/advisories/GHSA-4fwh-wrm6-97xm web
- https://github.com/klever-io/klever-go/pull/76 web
- https://github.com/klever-io/klever-go/commit/b8af922e749ec1fa7451b2c6e7c9f634d8603be2 web
- https://github.com/klever-io/klever-go package
- https://github.com/klever-io/klever-go/releases/tag/v1.7.20 web