CVE-2026-82405: Klever-Go Account takeover `kleverUpdateAccountPermission` Authorizes On Attacker-controlled `RecipientAddr` Vulnerability

HIGH
Advisory Identifier: GHSA-97cv-x867-6xhm
CWE-863
Published
Advisory Summary

CVE-2026-82405 is a HIGH `kleverupdateaccountpermission` authorizes on attacker-controlled `recipientaddr` vulnerability (CWE-863: Incorrect Authorization) affecting Klever-Go Account takeover libraries in the Go ecosystem. The issue involves `kleverupdateaccountpermission` authorizes on attacker-controlled `recipientaddr` instead of the authenticated caller. Remediation is available by upgrading affected packages to version 1.7.20.

CVE-2026-82405 at a Glance

CVE Identifier CVE-2026-82405
GHSA Identifier GHSA-97cv-x867-6xhm
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-863: Incorrect Authorization
Affected Product Klever-Go Account takeover
Package Ecosystem Go
Published Date
Last Updated

How to Fix CVE-2026-82405

To remediate this vulnerability, upgrade all affected Klever-Go Account takeover dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/klever-io/klever-go fixed: 1.7.20
Upgrade github.com/klever-io/klever-go to 1.7.20
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/klever-io/klever-go

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-82405, inspect your project dependencies for Klever-Go Account takeover:

inspect dependencies for github.com/klever-io/klever-go

Analyze the terminal output to locate any version of Klever-Go Account takeover in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.7.20), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-863: Incorrect Authorization, where 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. In Klever-Go Account takeover, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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: 1. **Wrong variable:** `CallerAddr` is never referenced in the handler; auth is `contractHasValidPermission(target.GetPermissions(), RecipientAddr)`, which returns true if `RecipientAddr` is a signer with `Weight >= Threshold` in the *target* account's permissions and the permission grants `UpdateAccountPermissionContractType`. 2. **RecipientAddr is attacker-controlled:** when a contract calls a built-in via `ExecuteOnDestContextWithTypedArgs` (`baseOps.go:1967`), `prepareIndirectContractCallInput` (`baseOps.go:2485`) sets `RecipientAddr = destination` (contract-chosen) and `CallerAddr = the calling contract`. The blockchain hook (`blockChainHook.go:454/467`) dispatches on `input.Function` and passes the input through unchanged; no guard forces `RecipientAddr == CallerAddr` and there is no SC-destination validation on this path. 3. **Self-signer default satisfies the check:** `createDefaultOwnerPermission` (`accounts.go:1848`) makes an account its own signer (weight 1, threshold 1, Owner type), and `CheckPermissionGrantedForContracts` returns true for Owner, so `contractHasValidPermission(V.perms, V) == true`. (More generally, `RecipientAddr` can be set to *any* of V's signer addresses meeting threshold all public on-chain.) Accounts with no stored permissions have empty `GetPermissions()` and are immune. 4. **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`: ```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: ```go 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`): ```go 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): ```go 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`: ```go 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`: ```go 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.

CVE-2026-82405 Affected Klever-Go Account takeover Versions

1 component(s)
Fixed Versions:
1.7.20
Product / Package Ecosystem Affected Versions Fixed Version
github.com/klever-io/klever-go
Go
< 1.7.20
1.7.20

References

Frequently Asked Questions

What is CVE-2026-82405?
CVE-2026-82405 is a security vulnerability in Klever-Go Account takeover involving `kleverUpdateAccountPermission` authorizes on attacker-controlled `RecipientAddr` instead of the authenticated caller under CWE-863: Incorrect Authorization.
Which Klever-Go Account takeover versions are affected?
Affected versions include releases prior to 1.7.20 across affected packages: github.com/klever-io/klever-go.
What version fixes CVE-2026-82405?
Upgrading to 1.7.20 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/klever-io/klever-go affected?
Yes, github.com/klever-io/klever-go is affected in releases prior to 1.7.20. Upgrade your dependency to 1.7.20 or later.
What CWE is associated with CVE-2026-82405?
CVE-2026-82405 is classified under 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 can I check Go dependencies for CVE-2026-82405?
Run the dependency tree command: 'inspect dependencies for github.com/klever-io/klever-go'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-97cv-x867-6xhm",
  "modified" : "2026-09-23T19:27:08Z",
  "published" : "2026-09-23T19:27:08Z",
  "aliases" : [ "CVE-2026-82405" ],
  "summary" : "Klever-Go Account takeover: `kleverUpdateAccountPermission` authorizes on attacker-controlled `RecipientAddr` instead of the authenticated caller",
  "details" : "### Description\n\nThe 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()`.\n\nMechanism:\n1. **Wrong variable:** `CallerAddr` is never referenced in the handler; auth is `contractHasValidPermission(target.GetPermissions(), RecipientAddr)`, which returns true if `RecipientAddr` is a signer with `Weight >= Threshold` in the *target* account's permissions and the permission grants `UpdateAccountPermissionContractType`.\n2. **RecipientAddr is attacker-controlled:** when a contract calls a built-in via `ExecuteOnDestContextWithTypedArgs` (`baseOps.go:1967`), `prepareIndirectContractCallInput` (`baseOps.go:2485`) sets `RecipientAddr = destination` (contract-chosen) and `CallerAddr = the calling contract`. The blockchain hook (`blockChainHook.go:454/467`) dispatches on `input.Function` and passes the input through unchanged; no guard forces `RecipientAddr == CallerAddr` and there is no SC-destination validation on this path.\n3. **Self-signer default satisfies the check:** `createDefaultOwnerPermission` (`accounts.go:1848`) makes an account its own signer (weight 1, threshold 1, Owner type), and `CheckPermissionGrantedForContracts` returns true for Owner, so `contractHasValidPermission(V.perms, V) == true`. (More generally, `RecipientAddr` can be set to *any* of V's signer addresses meeting threshold all public on-chain.) Accounts with no stored permissions have empty `GetPermissions()` and are immune.\n4. **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.\n\n### Code walkthrough\n\n**(a) The vulnerable handler** — `core/kapp/builtInFunctions/kleverUpdateAccountPermission.go`:\n```go\nfunc (e *kleverUpdateAccountPermission) ProcessBuiltinFunction(vmInput *vmcommon.ContractCallInput) (*vmcommon.VMOutput, error) {\n    ...\n    address := vmInput.NextArg()                       // Arguments[0] — attacker-chosen target account V\n    contract, err := e.getUpdateAccountPermissionContract(vmInput) // Arguments[1] — attacker-chosen new permissions\n    ...\n    acc, err := e.accountsCacher.LoadUser(address)      // loads V\n    ...\n    // BUG: authorizes against vmInput.RecipientAddr (attacker-controlled), NOT vmInput.CallerAddr\n    if !e.contractHasValidPermission(acc.GetPermissions(), vmInput.RecipientAddr) {   // L91\n        return nil, errors.New(\"invalid permission operation\")\n    }\n    // overwrites V's entire permission set with attacker-supplied signers\n    resultCode, err := e.kappController.GetAccountsKApp().UpdatePermission(address, contract)\n    ...\n}\n```\n\n**(b) The check just name-matches `recipientAddr` against V's own signers** — same file:\n```go\nfunc (e *kleverUpdateAccountPermission) contractHasValidPermission(permissions []*state.Permission, recipientAddr []byte) bool {\n    for _, permission := range permissions {\n        for _, signer := range permission.Signers {\n            if !bytes.Equal(signer.Address, recipientAddr) {   // recipientAddr, not the authenticated caller\n                continue\n            }\n            if signer.Weight >= permission.Threshold &&\n                permission.CheckPermissionGrantedForContracts(transaction.TXContract_UpdateAccountPermissionContractType) {\n                return true\n            }\n        }\n    }\n    return false\n}\n```\n\n**(c) The dispatch makes `RecipientAddr` attacker-controlled** — `kvm/vmhost/vmhooks/baseOps.go:2464` `prepareIndirectContractCallInput` (invoked when a contract calls the built-in via `ExecuteOnDestContext`):\n```go\ncontractCallInput := &vmcommon.ContractCallInput{\n    VMInput: vmcommon.VMInput{\n        CallerAddr: sender,        // the calling contract (authenticated) — NOT used by the handler\n        Arguments:  data,          // attacker-chosen: [V, attackerPermissions]\n        ...\n    },\n    RecipientAddr: destination,    // the contract's chosen `dest` argument — attacker sets this to V\n    Function:      string(function),\n}\n```\n\n**(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):\n```go\nreturn &state.Permission{\n    Type:      state.Permission_Owner,   // Owner grants ALL contract types incl. type 22\n    Threshold: 1,\n    Signers: []*state.Key{\n        { Address: ownerAcc.AddressBytes(), Weight: 1 },   // the account signs for itself\n    },\n}\n```\nSo `contractHasValidPermission(V.perms, RecipientAddr=V)` finds V's own address as a `Weight 1 >= Threshold 1` Owner signer → returns `true`.\n\n**(e) Contrast — the sibling handler does it correctly** — `core/kapp/builtInFunctions/kleverChangeOwnerAddress.go:86`:\n```go\ncallerAddress := vmInput.CallerAddr                        // authenticated caller\n...\nif !bytes.Equal(callerAddress, acc.GetOwnerAddress()) {    // checks the CALLER, not RecipientAddr\n    return nil, ErrOperationNotPermitted\n}\n```\n\n**Putting it together — the attacker's contract call:**\n```\nExecuteOnDestContext(\n    gas, dest = V,                       // → RecipientAddr = V\n    value = 0,\n    function = \"KleverUpdateAccountPermission\",\n    args = [ V, attackerOwnerPermsWithOnlyAttackerKey ],   // Arguments[0]=V, Arguments[1]=new perms\n)\n```\n→ `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.\n\n### POC\n\nput the following poc testcase under /core/kapp/builtInFunctions/ \n\nPOC Code: https://gist.github.com/mabdullah22/a41f90aa5ba86bbebf121f739bd5f5e9\n\nRun:\n```\ncd klever-go\nGOTOOLCHAIN=auto go test ./core/kapp/builtInFunctions/ -run TestPoC_PermTakeover -v\n```\nOutput:\n```\nTAKEOVER CONFIRMED: caller=\"attacker-contract\" (attacker SC) rewrote account V=\"victim-account-V\"; new sole owner signer=\"attacker-key-EVIL\"\n--- PASS: TestPoC_PermTakeover\n--- PASS: TestPoC_PermTakeover_NoStoredPermsIsSafe\n```\nThe 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.\n\n### Impact\n\nFull 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.\n\nSeverity **Critical**: Impact High (full account/asset compromise)\n\n### Recommendation\n\nAuthorize against the authenticated caller, mirroring `kleverChangeOwnerAddress`:\n```go\nif !e.contractHasValidPermission(acc.GetPermissions(), vmInput.CallerAddr) { ... }\n```\nReconcile 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.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/klever-io/klever-go"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.7.20"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.7.19"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/klever-io/klever-go/security/advisories/GHSA-97cv-x867-6xhm"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/klever-io/klever-go/commit/c58740eb74d7ee8f07db1e18a7d6214b5559ba32"
  }, {
    "type" : "WEB",
    "url" : "https://gist.github.com/mabdullah22/a41f90aa5ba86bbebf121f739bd5f5e9"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/klever-io/klever-go"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/klever-io/klever-go/releases/tag/v1.7.20"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-863" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-23T19:27:08Z",
    "nvd_published_at" : null
  }
}