GHSA-rm5c-5x2p-48wr Klever-Go KVM: Unauthenticated remote node crash (nil-pointer DoS) in klever-go P2P transaction interceptor (txVersionChecker nil RawData) - potential chain halt
SummaryEvery 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 componentRoot 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`)DetailsSynchronous validation path, no recovery at any frame:Root cause (`core/versioning/txVersionChecker.go`):`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.PreconditionsAttacker 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.ImpactDeterministic, 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 complexityCost: negligible (one self-signed libp2p peer; 3-byte payload; no gas/capital).Complexity: LOW. Unauthenticated, remote, deterministic.PoC-SourceScenarioBuild 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`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.)Save the source below as `core/process/interceptors/poc_nil_rawdata_dos_test.go`.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`):PoC-ResultsResult A - production `MultiDataInterceptor` (the `transactions` gossip topic):Result B - generic `SingleDataInterceptor` (same root cause via the shared chain):InterpretationBoth 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 fixPrimary (root cause) - make `CheckTxVersion` nil-safe / reject `RawData == nil` early: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.
CVSS v3.1 score: 7.5