Unverified sealing-block finalization lets a validator redirect a syncing node's replication requests
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 45/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- go
- Área
- distributed-systems, security
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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
- Lenguaje dominante
- Go
- Estrellas
- 22
- Forks
- 4
- Merge medio
- 1 d 16 h
- PR fusionados (30 d)
- 35
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de ava-labs/Simplex
-
Epoch discovery accepts a sealing block on a head count, letting low-stake validators forge epochsPosiblemente ocupada @yacovm la tomó hace 2 días. Abiertomedium security
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
Los mantenedores suelen responder en 1 día
-
Failed notarization WAL write lets a node sign both a finalize vote and an empty votePosiblemente ocupada @yacovm la tomó hace 4 días. Abiertomedium security
Dificultad 4/5 3-5 días Aptitud para principiantes 38/100
Los mantenedores suelen responder en 1 día
-
Builder ignores parent's P-chain height, so a leader's maximal height stalls honest proposalsPosiblemente ocupada @yacovm la tomó hace 4 días. Abiertomedium security
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
Los mantenedores suelen responder en 1 día
-
Honest builders never raise timestamp to parent's, so a leader's future timestamp voids honest proposalsPosiblemente ocupada @yacovm la tomó hace 4 días. Abiertohigh security
Dificultad 4/5 3-5 días Aptitud para principiantes 43/100
Los mantenedores suelen responder en 1 día
-
Auxiliary-info store keeps unbounded data from any unauthenticated peerPosiblemente ocupada @yacovm la tomó hace 4 días. Abiertohigh security
Dificultad 4/5 3-5 días Aptitud para principiantes 56/100
ava-labs/Simplex#652 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de ava-labs/Simplex
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
hashicorp/golang-lru#245 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
spotify/flink-on-k8s-operator#1075 ·
-
Dificultad 2/5 Medio día Aptitud para principiantes 62/100
prometheus-community/pyrra#1995 ·
Los mantenedores suelen responder en 1 día
-
API: `tsc.exe` opens a visible console window on Windows when the parent process has no consoleAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
microsoft/TypeScript#65083 ·
Los mantenedores suelen responder en 1 día