Unverified sealing-block finalization lets a validator redirect a syncing node's replication requests

Abierto
#666 0 comentarios 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@yacovm ya está trabajando en esto.

Desde el 30/10/2026.

  • #681 de @yacovm — abierto

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

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

low security

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
  1. 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().
  2. Byzantine validator sends a ReplicationResponse with 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. maybeObserveThresholdResponses counts only the epoch and digest, so the attached finalization plays no part. (nonvalidator/epochs.go:173-220)
  3. the node's processEpochReplicationQuorumRound passes the completing sender's quorum round to processSealingBlock. The comment in the code acknowledges that this finalization has not been verified. (nonvalidator/non_validator.go:209-242)
  4. storeQuorumRound stores the sealing block's quorum round without calling VerifyQC. verifyQuorumRound only checks that the finalization header equals the block header, and anyone can copy that header. (nonvalidator/non_validator.go:750-758)
  5. ReplicationState.StoreQuorumRound takes the signer list from the unverified QC and adopts it as highestObserved. newSignedQuorum reads signers from Finalization.QC.Signers(). The newest sealing block is the first and highest entry stored, so its signers win. (simplex/requestor.go:180-251)
  6. the requestor's sendRequests addresses every batch and every timeout re-send to the forged signer IDs. If the list is empty, BatchSequences returns nil and nothing is requested at all. (simplex/requestor.go:180-251)
  7. processReplicationState never 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

  1. nonvalidator/non_validator.go:244–251
  2. nonvalidator/non_validator.go:750–758
  3. nonvalidator/non_validator.go:209–242
  4. nonvalidator/epochs.go:173–220
  5. nonvalidator/non_validator.go:732–747
  6. simplex/replication_state.go:135–187
  7. simplex/requestor.go:24–40
  8. simplex/requestor.go:180–251
  9. nonvalidator/non_validator.go:659–692
  10. 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

  1. 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 replace highestObserved, 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

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de ava-labs/Simplex

Todos los issues de ava-labs/Simplex

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.