Skip to content

Bug Bounty Submission: Broken Replay Protection in Bridged Reputation Root Hash Update due to Incorrect Nonce Keying #1327

@loopghost

Description

@loopghost

Bug Bounty Submission: Critical – Broken Replay Protection in Bridged Reputation Root Hash Update due to Incorrect Nonce Keying

Prerequisite

I ensured that this issue, specifically the broken replay protection for setReputationRootHashFromBridge caused by using block.chainid as the nonce map key, has not already been reported.

Summary

The setReputationRootHashFromBridge function in ColonyNetworkMining.sol is intended to update the global reputationRootHash based on messages received from another chain (e.g., the mining chain) via the bridge. It includes a nonce mechanism (bridgeCurrentRootHashNonces) presumably to prevent replay attacks. However, the implementation critically uses block.chainid (the ID of the current, receiving chain) as the key to check and update the nonce state. For cross-chain messages, the nonce check must be performed against state associated with the source chain ID to be effective. By using block.chainid, the check becomes non-functional for its intended purpose, allowing valid messages (VAAs) from the source chain to be replayed potentially indefinitely until a message happens to affect the nonce stored under the block.chainid key (which may never happen via this function). This breaks a critical security primitive (replay protection) for cross-chain state synchronization, allowing potential manipulation of the global reputationRootHash. This constitutes a Critical Risk vulnerability according to the OWASP Risk Rating methodology.

Steps to Reproduce (Conceptual PoC)

This PoC illustrates how replay is possible because the nonce check uses the wrong key. Let Chain M be the source/mining chain and Chain C be the target/non-mining chain where setReputationRootHashFromBridge executes.

  • Initial State: Chain C has a state variable mapping(uint256 => uint256) bridgeCurrentRootHashNonces. Let bridgeCurrentRootHashNonces[<Chain_C_ID>] = X (the value stored under the current chain's ID key, which might be 0 or some unrelated value). The current reputation state is HASH_OLD.

  • Valid Message Sent: Chain M sends Msg1 = (HASH_A, LEAVES_A, nonce=N+1) via the bridge to Chain C. Assume this message (VAA) is valid according to Wormhole and the configured emitter address for Chain M.

  • First Processing on Chain C:

    • WormholeBridgeForColony calls ColonyNetworkMining.setReputationRootHashFromBridge(HASH_A, LEAVES_A, N+1).
    • Inside setReputationRootHashFromBridge:
      • The check require(_nonce (N+1) >= bridgeCurrentRootHashNonces[block.chainid] (X)) is performed. Since X is unrelated to the sequence of messages from Chain M (it might be 0), this check is highly likely to pass for any valid N+1 >= 0. Let's assume it passes.
      • State updated: reputationRootHash = HASH_A.
      • Nonce state updated: bridgeCurrentRootHashNonces[block.chainid] = N+1. (The value for key Chain C ID is now N+1).
  • Replay Attempt: An attacker resubmits the exact same valid Msg1 (containing HASH_A, LEAVES_A, nonce=N+1) to Chain C.

  • Second (Replay) Processing on Chain C:

    • WormholeBridgeForColony calls ColonyNetworkMining.setReputationRootHashFromBridge(HASH_A, LEAVES_A, N+1) again.
    • Inside setReputationRootHashFromBridge:
      • The check require(_nonce (N+1) >= bridgeCurrentRootHashNonces[block.chainid] (N+1)) is performed. This passes (due to >=).
      • State HASH_A is reapplied. Nonce state bridgeCurrentRootHashNonces[block.chainid] is set to N+1 again. Gas is consumed.
  • Further Replays: The attacker can continue replaying Msg1 indefinitely. The check will always be N+1 >= N+1, which always passes.

  • Replay of Older Message: Assume Chain M later sends Msg2 = (HASH_B, LEAVES_B, nonce=N+2), which is processed, setting bridgeCurrentRootHashNonces[block.chainid] = N+2. If the attacker tries to replay Msg1 (nonce=N+1) now, the check N+1 >= N+2 fails. However, if they had an even older valid message, say Msg0 = (HASH_OLD, LEAVES_OLD, nonce=N), they could potentially replay that message if N >= bridgeCurrentRootHashNonces[block.chainid] somehow becomes true due to unrelated activity setting that nonce value. The core issue is that the check isn't reliably tied to the source chain's message sequence.

Expected Behavior

The nonce check within setReputationRootHashFromBridge should use a state variable keyed by the source chain ID of the incoming message (e.g., wormholeMessage.emitterChainId obtained from the parsed VAA, which would need to be passed down or accessed). The check should be require(_nonce > lastProcessedNonceForSourceChain) to prevent any replays, including same-nonce replays.

// Conceptual Correct Logic:
// Assuming 'sourceChainId' is available
uint256 lastProcessedNonce = bridgeCurrentRootHashNonces[sourceChainId]; // Use source ID as key
require(_nonce > lastProcessedNonce, "colony-mining-bridge-nonce-must-be-greater");
bridgeCurrentRootHashNonces[sourceChainId] = _nonce; // Update state for source ID

Current Behaviour

The function uses block.chainid (the ID of the current/receiving chain) as the key for checking and updating the nonce map (bridgeCurrentRootHashNonces). This disconnects the nonce check from the actual sequence of messages originating from the source chain, effectively disabling replay protection against messages from other chains and allowing potentially unlimited replays of the same valid message.

Possible Solution

  • Refactor State: The bridgeCurrentRootHashNonces mapping should ideally be keyed by the source chain ID, not the current chain ID.

  • Pass Source Chain ID: The setReputationRootHashFromBridge function needs access to the source chain ID of the message. This might require modifying the bridge interaction to pass this information alongside the payload, or potentially accessing it from the VAA context if possible within the execution flow (less likely given the current structure).

  • Implement Strict Check: Use require(_nonce > bridgeCurrentRootHashNonces[sourceChainId], ...).

Context

  • OWASP Risk Rating:

    • Likelihood: High. The logical flaw (using block.chainid key) is present in the code. Exploitation involves replaying a valid cross-chain message (VAA), which is a known attack vector against bridges if replay protection is flawed.
    • Impact: High. Breaking replay protection for a function that sets a critical network-wide state variable (reputationRootHash) severely impacts the system's integrity. It could allow an attacker to repeatedly impose an older or specific state, potentially disrupting reputation-based mechanisms, invalidating legitimate updates that occurred after the replayed message's original time, or causing downstream issues in systems relying on this state.
    • Overall OWASP Risk: High (Likelihood) * High (Impact) = Critical Risk.
  • Severity Classification: Based strictly on the OWASP Risk Rating methodology, a Critical Risk corresponds to a Critical Severity classification for this vulnerability, due to the potential manipulation of core network state via broken replay protection.

Environment

  • Operating System: N/A (Blockchain/Solidity bug)
  • Ethereum client: N/A (Applies to any EVM chain where deployed)
  • solc version: 0.8.28 (as specified in pragmas)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions