Denial of Service in TenderMint
Description
TenderMint from version 0.33.0 and before version 0.33.6 allows block proposers to include signatures for the wrong block. This may happen naturally if you start a network, have it run for some time and restart it (without changing chainID). A malicious block proposer (even with a minimal amount of stake) can use this vulnerability to completely halt the network. This issue is fixed in Tendermint 0.33.6 which checks all the signatures are for the block with 2/3+ majority before creating a commit.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/tendermint/tendermintGo | < 0.33.6 | 0.33.6 |
Affected products
1- Range: >= 0.33.0, < 0.33.6
Patches
1480b995a3172consensus: Do not allow signatures for a wrong block in commits
2 files changed · +108 −4
consensus/invalid_test.go+97 −0 added@@ -0,0 +1,97 @@ +package consensus + +import ( + "testing" + + "github.com/tendermint/tendermint/libs/bytes" + "github.com/tendermint/tendermint/libs/log" + tmrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/p2p" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + "github.com/tendermint/tendermint/types" +) + +//---------------------------------------------- +// byzantine failures + +// one byz val sends a precommit for a random block at each height +// Ensure a testnet makes blocks +func TestReactorInvalidPrecommit(t *testing.T) { + N := 4 + css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) + defer cleanup() + + for i := 0; i < 4; i++ { + ticker := NewTimeoutTicker() + ticker.SetLogger(css[i].Logger) + css[i].SetTimeoutTicker(ticker) + + } + + reactors, blocksSubs, eventBuses := startConsensusNet(t, css, N) + + // this val sends a random precommit at each height + byzValIdx := 0 + byzVal := css[byzValIdx] + byzR := reactors[byzValIdx] + + // update the doPrevote function to just send a valid precommit for a random block + // and otherwise disable the priv validator + byzVal.mtx.Lock() + pv := byzVal.privValidator + byzVal.doPrevote = func(height int64, round int32) { + invalidDoPrevoteFunc(t, height, round, byzVal, byzR.Switch, pv) + } + byzVal.mtx.Unlock() + defer stopConsensusNet(log.TestingLogger(), reactors, eventBuses) + + // wait for a bunch of blocks + // TODO: make this tighter by ensuring the halt happens by block 2 + for i := 0; i < 10; i++ { + timeoutWaitGroup(t, N, func(j int) { + <-blocksSubs[j].Out() + }, css) + } +} + +func invalidDoPrevoteFunc(t *testing.T, height int64, round int32, cs *State, sw *p2p.Switch, pv types.PrivValidator) { + // routine to: + // - precommit for a random block + // - send precommit to all peers + // - disable privValidator (so we don't do normal precommits) + go func() { + cs.mtx.Lock() + cs.privValidator = pv + pubKey, err := cs.privValidator.GetPubKey() + if err != nil { + panic(err) + } + addr := pubKey.Address() + valIndex, _ := cs.Validators.GetByAddress(addr) + + // precommit a random block + blockHash := bytes.HexBytes(tmrand.Bytes(32)) + precommit := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: valIndex, + Height: cs.Height, + Round: cs.Round, + Timestamp: cs.voteTime(), + Type: tmproto.PrecommitType, + BlockID: types.BlockID{ + Hash: blockHash, + PartSetHeader: types.PartSetHeader{Total: 1, Hash: tmrand.Bytes(32)}}, + } + p := precommit.ToProto() + cs.privValidator.SignVote(cs.state.ChainID, p) + precommit.Signature = p.Signature + cs.privValidator = nil // disable priv val so we don't do normal votes + cs.mtx.Unlock() + + peers := sw.Peers().List() + for _, peer := range peers { + cs.Logger.Info("Sending bad vote", "block", blockHash, "peer", peer) + peer.Send(VoteChannel, MustEncode(&VoteMessage{precommit})) + } + }() +}
types/vote_set.go+11 −4 modified@@ -549,9 +549,11 @@ func (voteSet *VoteSet) sumTotalFrac() (int64, int64, float64) { //-------------------------------------------------------------------------------- // Commit -// MakeCommit constructs a Commit from the VoteSet. -// Panics if the vote type is not PrecommitType or if -// there's no +2/3 votes for a single block. +// MakeCommit constructs a Commit from the VoteSet. It only includes precommits +// for the block, which has 2/3+ majority, and nil. +// +// Panics if the vote type is not PrecommitType or if there's no +2/3 votes for +// a single block. func (voteSet *VoteSet) MakeCommit() *Commit { if voteSet.signedMsgType != tmproto.PrecommitType { panic("Cannot MakeCommit() unless VoteSet.Type is PrecommitType") @@ -567,7 +569,12 @@ func (voteSet *VoteSet) MakeCommit() *Commit { // For every validator, get the precommit commitSigs := make([]CommitSig, len(voteSet.votes)) for i, v := range voteSet.votes { - commitSigs[i] = v.CommitSig() + commitSig := v.CommitSig() + // if block ID exists but doesn't match, exclude sig + if commitSig.ForBlock() && !v.BlockID.Equals(*voteSet.maj23) { + commitSig = NewCommitSigAbsent() + } + commitSigs[i] = commitSig } return NewCommit(voteSet.GetHeight(), voteSet.GetRound(), *voteSet.maj23, commitSigs)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-6jqj-f58p-mrw3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-15091ghsaADVISORY
- github.com/tendermint/tendermint/blob/master/CHANGELOG.mdghsaWEB
- github.com/tendermint/tendermint/commit/480b995a31727593f58b361af979054d17d84340ghsax_refsource_MISCWEB
- github.com/tendermint/tendermint/issues/4926ghsax_refsource_MISCWEB
- github.com/tendermint/tendermint/pull/5426ghsaWEB
- github.com/tendermint/tendermint/security/advisories/GHSA-6jqj-f58p-mrw3ghsax_refsource_CONFIRMWEB
- pkg.go.dev/vuln/GO-2021-0090ghsaWEB
News mentions
0No linked articles in our index yet.