Unverified sealing-block finalization lets a validator redirect a syncing node's replication requests
メンテナーはふだん 1 日以内に返信
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 45/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- go
調査の方向性
Start with nonvalidator/non_validator.go around processEpochReplicationQuorumRound, processSealingBlock, and storeQuorumRound, then trace how simplex/replication_state.go and simplex/requestor.go adopt QC signers for request routing. Add a test for an f+1-th sealing-block response with forged signers. Done means its signers do not change highestObserved and later replication requests still reach genuine validators.
索引モデルが issue の本文から書いたものです。
説明
Details
A Byzantine validator can stall a lagging or new non-validator node's catch-up, one node per race it wins. It must be in the latest validator set and its response must be the f+1-th matching sealing-block response. The cause is that processSealingBlock stores the finalization without checking its signatures, so its signer list decides where later requests go.
flowchart TD
n1["Byzantine validator"]
n2["processSealingBlock and storeQuorumRound"]
n3["VerifyQC on sealing-block finalization"]
n4["requestor highestObserved signers"]
n5["syncing node's catch-up"]
n1 -->|"sends forged finalization as f+1-th response"| n2
n3 -->|"not applied"| n2
n2 -->|"stores forged signer list"| n4
n4 -->|"sends requests to attacker-chosen IDs, stalling it"| n5
classDef actor stroke:#2563eb,stroke-width:2px
classDef vulnerable_code stroke:#dc2626,stroke-width:2px
classDef existing_control stroke:#16a34a,stroke-width:2px
classDef missing_control stroke:#dc2626,stroke-width:2px,stroke-dasharray:5 5
classDef asset stroke:#9333ea,stroke-width:2px
classDef component stroke:#6b7280,stroke-width:1px
class n1 actor
class n2 vulnerable_code
class n3 missing_control
class n4 component
class n5 asset
- Byzantine validator waits for a lagging or new node to start epoch replication. The validator must be in the latest validator set, because the threshold collector only counts senders listed in
Comm.Validators(). - Byzantine validator sends a
ReplicationResponsewith the genuine newest sealing block and a forged finalization. The forged QC lists only the attacker, or nobody usable, as signers, and the response must be the f+1-th matching one, which is a timing race.maybeObserveThresholdResponsescounts only the epoch and digest, so the attached finalization plays no part. (nonvalidator/epochs.go:173-220) - the node's
processEpochReplicationQuorumRoundpasses the completing sender's quorum round toprocessSealingBlock. The comment in the code acknowledges that this finalization has not been verified. (nonvalidator/non_validator.go:209-242) storeQuorumRoundstores the sealing block's quorum round without callingVerifyQC.verifyQuorumRoundonly checks that the finalization header equals the block header, and anyone can copy that header. (nonvalidator/non_validator.go:750-758)ReplicationState.StoreQuorumRoundtakes the signer list from the unverified QC and adopts it ashighestObserved.newSignedQuorumreadssignersfromFinalization.QC.Signers(). The newest sealing block is the first and highest entry stored, so its signers win. (simplex/requestor.go:180-251)- the requestor's
sendRequestsaddresses every batch and every timeout re-send to the forged signer IDs. If the list is empty,BatchSequencesreturns nil and nothing is requested at all. (simplex/requestor.go:180-251) processReplicationStatenever reaches the point where the forged finalization is checked. A stored QC is verified only when its sequence is next to commit, and a node whose requests go to the wrong IDs never gets there. (nonvalidator/non_validator.go:659-692)
Evidence
- nonvalidator/non_validator.go:244–251
- nonvalidator/non_validator.go:750–758
- nonvalidator/non_validator.go:209–242
- nonvalidator/epochs.go:173–220
- nonvalidator/non_validator.go:732–747
- simplex/replication_state.go:135–187
- simplex/requestor.go:24–40
- simplex/requestor.go:180–251
- nonvalidator/non_validator.go:659–692
- nonvalidator/non_validator.go:783–805
Impact
The victim node's catch-up stalls until a verified higher finalization reaches it through handleFinalization. On an idle chain, or when the node's vote is needed to finalize new blocks, the stall can persist, and a lagging validator cannot rejoin consensus meanwhile. Safety holds: the fake finalization is never indexed, because it fails VerifyQC when it is next to commit. Each won race affects one node.
Reproduction steps
- The attacker is a Byzantine validator in the latest validator set. It waits for a lagging or new non-validator node to enter epoch replication, the first phase of catching up. It must win a timing race, because its response has to be the one that completes the f+1 threshold for the newest sealing block. The report names a second route: any peer can attach fake finalizations to older sealing blocks through the hash-chain branch (
canValidate). That variant alone is minor. Those sequences are lower, so they never replacehighestObserved, and each costs only one re-request (:685) once reached.
Recommended fix
During epoch replication, sealing-block quorum rounds go into the replication state without their finalization being verified. The unverified QC's signer list then becomes highestObserved, which decides where all replication requests are sent.
Fix criteria: No finalization, and no signer list taken from it, enters the replication state or the requestor's routing until it verifies against the validator set of its own epoch. Alternatively, routing uses a trusted set such as Comm.Validators(). A test confirms it: when the f+1-th sealing-block response carries a QC with forged signers, highestObserved.signers is unchanged. Later ReplicationRequests still go to genuine validators.
Status on main
Re-checked against main at 90995e0 on 2026-10-26; still present:
During epoch replication, processEpochReplicationQuorumRound and handleQrFromUnknownEpoch call processSealingBlock (nonvalidator/non_validator.go:221-231, 795-804), which calls storeQuorumRound (non_validator.go:248-252). That path stores sealing blocks without VerifyQC (non_validator.go:755-758); verifyQuorumRound only checks that the header is consistent (non_validator.go:734-747). ReplicationState.StoreQuorumRound then calls receivedSignedQuorum(newSignedQuorum(...)), which adopts the unverified QC.Signers() as highestObserved (simplex/replication_state.go:183-186, simplex/requestor.go:24-40, 239-245), and sendRequests routes every batch and resend to those signers (requestor.go:177-188). Only the next-to-commit VerifyQC failure falls back to Comm.Validators() (non_validator.go:681-686), and that fallback does not reset highestObserved.
Related: #442 (open, related: requests should go to validators)
Severity: LOW
Status: Open
Category: Improper signature verification
CWE: CWE-347
Repository: ava-labs/Simplex
Branch: main
Date created: 2026-10-26
Claude Security finding: https://claude.ai/security/findings/find_01Y4JZoVbjowYqTefsKBxVNQ
- 主要言語
- Go
- スター
- 22
- フォーク
- 4
- 平均マージ
- 1日 16時間
- マージ済み PR(30日)
- 35
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
ava-labs/Simplex のほかの issue
-
Epoch discovery accepts a sealing block on a head count, letting low-stake validators forge epochs対応中かも @yacovm が 1 日前に担当しました。 オープンmedium security
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
-
Failed notarization WAL write lets a node sign both a finalize vote and an empty vote対応中かも @yacovm が 3 日前に担当しました。 オープンmedium security
難易度 4/5 3〜5日 初心者へのやさしさ 38/100
メンテナーはふだん 1 日以内に返信
-
Builder ignores parent's P-chain height, so a leader's maximal height stalls honest proposals対応中かも @yacovm が 3 日前に担当しました。 オープンmedium security
難易度 4/5 3〜5日 初心者へのやさしさ 55/100
メンテナーはふだん 1 日以内に返信
-
Honest builders never raise timestamp to parent's, so a leader's future timestamp voids honest proposals対応中かも @yacovm が 3 日前に担当しました。 オープンhigh security
難易度 4/5 3〜5日 初心者へのやさしさ 43/100
メンテナーはふだん 1 日以内に返信
-
Auxiliary-info store keeps unbounded data from any unauthenticated peer対応中かも @yacovm が 3 日前に担当しました。 オープンhigh security
難易度 4/5 3〜5日 初心者へのやさしさ 56/100
ava-labs/Simplex#652 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
ava-labs/Simplex の issue をすべて見る
似ている issue
-
docs(prompts): IF BLOCKED workflow-scope branch says to comment on the issue on jira/local trackersオープンagent-butler-finding bug
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
jordansmall/spindrift#6238 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
microcks/microcks-cli#608 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug from-studio
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
esengine/DeepSeek-Reasonix#14868 ·
メンテナーはふだん 1 日以内に返信
-
Install notes print "Camunda REST API: https://" without a host when global.host is set対応中かも @irisfield が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
camunda/camunda-platform-helm#7790 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
wso2/agent-manager#2220 ·
メンテナーはふだん 1 日以内に返信