Skip to content

[Security][tentative] Medium (attackable): Pending Checkpoint Digest Is Not Validated #253

@evonide

Description

@evonide

Context

Consensus state can carry a pending_checkpoint that will later be used to finalize a checkpoint artifact. The intended invariant is that each checkpoint's digest is the hash of its embedded data, because trustless recovery compares finalized checkpoint hashes against the checkpoint bytes.

Claim

Decoded checkpoints carry data and digest as independent fields, and ConsensusState.pending_checkpoint decoding accepts the embedded digest without checking it matches the embedded data. The finalizer later uses that stored digest directly as the epoch-final block's checkpoint hash.

A malicious unchecked checkpoint artifact supplier or local decoded-state source can provide consensus state containing a pending_checkpoint whose stored digest differs from its data hash, and finalization later copies that unvalidated digest into the checkpoint hash so the exported checkpoint artifact fails trustless verification. A trustlessly verified checkpoint is not an arbitrary edit surface unless the signed checkpoint data itself already contains the malformed embedded checkpoint.

Flow

The path is reachable through consensus-state decoding when pending_checkpoint is present. It requires malformed serialized state, an explicitly trusted checkpoint loaded without verification headers, or canonical signed checkpoint data that already contains pending_checkpoint.data = X and pending_checkpoint.digest = Y where Y != sha256(X), followed by finalization that consumes the pending checkpoint. Startup verifies the outer checkpoint only when finalized headers are supplied.

Impact

Honest proposers that start from or otherwise decode such malformed state can finalize a header committing Y, while the stored/exported checkpoint artifact contains X. Future trustless importers reject the checkpoint because the signed header hash does not match the checkpoint data hash, making that epoch's checkpoint unusable for recovery.

Root Cause

The Checkpoint type stores a redundant digest but decode paths do not rederive and validate it before the finalizer trusts it as the canonical checkpoint hash.

Code

Related Issues/PRs

Related issues cover adjacent checkpoint startup, state-position binding, transition queue, genesis binding, and pending-checkpoint root omission risks.

Fix

Make Checkpoint::read_cfg or Checkpoint::from_ssz_bytes validate digest == sha256(data). Recompute the checkpoint hash from checkpoint.data when constructing aux data and when storing finalized checkpoints. Add tests for malformed embedded pending_checkpoint values in decoded ConsensusState.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions