GHSA-97cv-x867-6xhm CVE-2026-82405 GO-2026-6582
Klever-Go Account takeover: `kleverUpdateAccountPermission` authorizes on attacker-controlled `RecipientAddr` instead of the authenticated caller
- Severity
- 0.0 / 10
- High · CVSS 4.0
- Exploit chance (EPSS)
- 0.43%
- Chance of exploitation in the next 30 days. Higher than 37.03% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-863 Incorrect Authorization
- Fix
1.7.20- Upgrade github.com/klever-io/klever-go to this version or later
Description
Description
The VM built-in function KleverUpdateAccountPermission (registered always-active, creator.go:381-390 / core/vmconstants.go:234) rewrites an account's entire permission set. Its authorization check uses vmInput.RecipientAddr attacker-controlled instead of the authenticated vmInput.CallerAddr. The sibling handler kleverChangeOwnerAddress.go:86 uses vmInput.CallerAddr correctly, so the safe pattern exists in-repo; this handler deviates. The native transaction path (txProcess.go:833) is safe it uses tx.GetSender().
Mechanism:
- Wrong variable:
CallerAddris never referenced in the handler; auth iscontractHasValidPermission(target.GetPermissions(), RecipientAddr), which returns true ifRecipientAddris a signer withWeight >= Thresholdin the target account's permissions and the permission grantsUpdateAccountPermissionContractType. - RecipientAddr is attacker-controlled: when a contract calls a built-in via
ExecuteOnDestContextWithTypedArgs(baseOps.go:1967),prepareIndirectContractCallInput(baseOps.go:2485) setsRecipientAddr = destination(contract-chosen) andCallerAddr = the calling contract. The blockchain hook (blockChainHook.go:454/467) dispatches oninput.Functionand passes the input through unchanged; no guard forcesRecipientAddr == CallerAddrand there is no SC-destination validation on this path. - Self-signer default satisfies the check:
createDefaultOwnerPermission(accounts.go:1848) makes an account its own signer (weight 1, threshold 1, Owner type), andCheckPermissionGrantedForContractsreturns true for Owner, socontractHasValidPermission(V.perms, V) == true. (More generally,RecipientAddrcan be set to any of V's signer addresses meeting threshold all public on-chain.) Accounts with no stored permissions have emptyGetPermissions()and are immune. - Overwrite is unrestricted:
UpdatePermission(V, attackerContract)(accounts.go:1863) replaces V's permission set with attacker-supplied signers; if the attacker supplies an Owner-type permission, no default is appended and V's prior control is fully evicted.
Code walkthrough
(a) The vulnerable handler — core/kapp/builtInFunctions/kleverUpdateAccountPermission.go:
func (e *kleverUpdateAccountPermission) ProcessBuiltinFunction(vmInput *vmcommon.ContractCallInput) (*vmcommon.VMOutput, error) {
...
address := vmInput.NextArg() // Arguments[0] — attacker-chosen target account V
contract, err := e.getUpdateAccountPermissionContract(vmInput) // Arguments[1] — attacker-chosen new permissions
...
acc, err := e.accountsCacher.LoadUser(address) // loads V
...
// BUG: authorizes against vmInput.RecipientAddr (attacker-controlled), NOT vmInput.CallerAddr
if !e.contractHasValidPermission(acc.GetPermissions(), vmInput.RecipientAddr) { // L91
return nil, errors.New("invalid permission operation")
}
// overwrites V's entire permission set with attacker-supplied signers
resultCode, err := e.kappController.GetAccountsKApp().UpdatePermission(address, contract)
...
}
(b) The check just name-matches recipientAddr against V's own signers — same file:
func (e *kleverUpdateAccountPermission) contractHasValidPermission(permissions []*state.Permission, recipientAddr []byte) bool {
for _, permission := range permissions {
for _, signer := range permission.Signers {
if !bytes.Equal(signer.Address, recipientAddr) { // recipientAddr, not the authenticated caller
continue
}
if signer.Weight >= permission.Threshold &&
permission.CheckPermissionGrantedForContracts(transaction.TXContract_UpdateAccountPermissionContractType) {
return true
}
}
}
return false
}
(c) The dispatch makes RecipientAddr attacker-controlled — kvm/vmhost/vmhooks/baseOps.go:2464 prepareIndirectContractCallInput (invoked when a contract calls the built-in via ExecuteOnDestContext):
contractCallInput := &vmcommon.ContractCallInput{
VMInput: vmcommon.VMInput{
CallerAddr: sender, // the calling contract (authenticated) — NOT used by the handler
Arguments: data, // attacker-chosen: [V, attackerPermissions]
...
},
RecipientAddr: destination, // the contract's chosen `dest` argument — attacker sets this to V
Function: string(function),
}
(d) Every account with configured permissions is its own signer — core/kapp/accounts/accounts.go:1848 createDefaultOwnerPermission (appended by UpdatePermission when no Owner permission is supplied):
return &state.Permission{
Type: state.Permission_Owner, // Owner grants ALL contract types incl. type 22
Threshold: 1,
Signers: []*state.Key{
{ Address: ownerAcc.AddressBytes(), Weight: 1 }, // the account signs for itself
},
}
So contractHasValidPermission(V.perms, RecipientAddr=V) finds V's own address as a Weight 1 >= Threshold 1 Owner signer → returns true.
(e) Contrast — the sibling handler does it correctly — core/kapp/builtInFunctions/kleverChangeOwnerAddress.go:86:
callerAddress := vmInput.CallerAddr // authenticated caller
...
if !bytes.Equal(callerAddress, acc.GetOwnerAddress()) { // checks the CALLER, not RecipientAddr
return nil, ErrOperationNotPermitted
}
Putting it together — the attacker's contract call:
ExecuteOnDestContext(
gas, dest = V, // → RecipientAddr = V
value = 0,
function = "KleverUpdateAccountPermission",
args = [ V, attackerOwnerPermsWithOnlyAttackerKey ], // Arguments[0]=V, Arguments[1]=new perms
)
→ CallerAddr = attackerContract (ignored), RecipientAddr = V, contractHasValidPermission(V.perms, V) == true → V's permissions overwritten with the attacker's key as sole Owner signer. The attacker never held a key of V and provided no signature from V.
POC
put the following poc testcase under /core/kapp/builtInFunctions/
POC Code: https://gist.github.com/mabdullah22/a41f90aa5ba86bbebf121f739bd5f5e9
Run:
cd klever-go
GOTOOLCHAIN=auto go test ./core/kapp/builtInFunctions/ -run TestPoC_PermTakeover -v
Output:
TAKEOVER CONFIRMED: caller="attacker-contract" (attacker SC) rewrote account V="victim-account-V"; new sole owner signer="attacker-key-EVIL"
--- PASS: TestPoC_PermTakeover
--- PASS: TestPoC_PermTakeover_NoStoredPermsIsSafe
The harm asserted is the takeover itself: after the call, V's permission set is a single Owner permission whose sole signer is the attacker's key; V's original owner signer is gone.
Impact
Full takeover of any account that has configured permissions i.e. every multisig / advanced-permission account , by an attacker who deploys a cheap smart contract and supplies only public on-chain addresses (no keys, no signatures from the victim). After takeover the attacker controls all of the victim's operations → theft or permanent lock of all the account's assets. Reachable via a permissionlessly-deployed contract (the plain-tx path is safe, so it is Critical-via-contract, not fully no-contract). No fork flag gates it.
Severity Critical: Impact High (full account/asset compromise)
Recommendation
Authorize against the authenticated caller, mirroring kleverChangeOwnerAddress:
if !e.contractHasValidPermission(acc.GetPermissions(), vmInput.CallerAddr) { ... }
Reconcile the SC-call authority model: on the built-in path CallerAddr is the calling contract, so a contract should only be able to update permissions of accounts that legitimately list it as an authorized signer — never an arbitrary victim. Consider also requiring the target account (Arguments[0]) to equal the authorized caller's account, matching the native tx.GetSender() model.
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:H/VI:H/VA:H/SC:N/SI:N/SA:N
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- Low
- User interaction
- None
- Confidentiality (vulnerable system)
- High
- Integrity (vulnerable system)
- High
- Availability (vulnerable system)
- High
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- None
- Availability (subsequent systems)
- None
Weakness
CWE-863: Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.
How to prevent it
- Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
- Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
- 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 authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
- Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
- System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why 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-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