34349 - [BC - High] Archiver Join Limit Logic Error

Submitted on Aug 9th 2024 at 22:50:30 UTC by @Lastc0de for Boost | Shardeum: Core

Report ID: #34349

Report type: Blockchain/DLT

Report severity: High

Target: https://github.com/shardeum/shardus-core/tree/dev

Impacts:

  • Network not being able to confirm new transactions (total network shutdown)

  • RPC API crash affecting projects with greater than or equal to 25% of the market capitalization on top of the respective layer

Description

Brief/Intro

Archivers can join network without any staking. Network has a max limit for archivers to join, but shardus-core has a bug that allows more than MAX limit archiver to join the network.

This bug can harm network in many ways, for example it disallows any other archiver from joining the network, or when a node wants to join/left the network, it finds a random archiver and requests some data from it, because a malicious actor can join it's archivers more than specified limit, it is possible that every time a node selects a random archiver that archiver is one of these malicious ones. So bad actor can return invalid data and break the network. Another example which i provided a POC for it, can completely disable archivers functionality to save Cycle data, so history of blockchain would be lost forever.

I will explain the problem here and provide a POC after.

Vulnerability Details

For an archiver to join the network, it should send a http request to a node. Node handles request here:

shardus-core/src/p2p/Archivers.ts

export function registerRoutes() {
  network.registerExternalPost('joinarchiver', async (req, res) => {
    const err = validateTypes(req, { body: 'o' })
    if (err) {
      warn(`joinarchiver: bad req ${err}`)
      return res.send({ success: false, error: err })
    }

    const joinRequest = req.body
    if (logFlags.p2pNonFatal) info(`Archiver join request received: ${Utils.safeStringify(joinRequest)}`)

    const accepted = await addArchiverJoinRequest(joinRequest)
...
  }
}

then addArchiverJoinRequest function is called which does some validations and adds join request to a list and propagates it to other nodes

src/shardus/index.ts

export function addArchiverJoinRequest(joinRequest: P2P.ArchiversTypes.Request, tracker?, gossip = true) {
  // validate input
  let err = validateTypes(joinRequest, { nodeInfo: 'o', requestType: 's', requestTimestamp: 'n', sign: 'o' })
  if (err) {
    warn('addJoinRequest: bad joinRequest ' + err)
    return { success: false, reason: 'bad joinRequest ' + err }
  }
  err = validateTypes(joinRequest.nodeInfo, {
    curvePk: 's',
    ip: 's',
    port: 'n',
    publicKey: 's',
  })
  if (err) {
    warn('addJoinRequest: bad joinRequest.nodeInfo ' + err)
    return { success: false, reason: 'bad joinRequest ' + err }
  }
  if (joinRequest.requestType !== P2P.ArchiversTypes.RequestTypes.JOIN) {
    warn('addJoinRequest: invalid joinRequest.requestType')
    return { success: false, reason: 'invalid joinRequest.requestType' }
  }
  err = validateTypes(joinRequest.sign, { owner: 's', sig: 's' })
  if (err) {
    warn('addJoinRequest: bad joinRequest.sign ' + err)
    return { success: false, reason: 'bad joinRequest.sign ' + err }
  }
  if (!crypto.verify(joinRequest, joinRequest.nodeInfo.publicKey)) {
    warn('addJoinRequest: bad signature')
    return { success: false, reason: 'bad signature ' }
  }
  if (archivers.get(joinRequest.nodeInfo.publicKey)) {
    warn('addJoinRequest: This archiver is already in the active archiver list')
    return { success: false, reason: 'This archiver is already in the active archiver list' }
  }
  const existingJoinRequest = joinRequests.find(
    (j) => j.nodeInfo.publicKey === joinRequest.nodeInfo.publicKey
  )
  if (existingJoinRequest) {
    warn('addJoinRequest: This archiver join request already exists')
    return { success: false, reason: 'This archiver join request already exists' }
  }
  if (Context.config.p2p.forceBogonFilteringOn) {
    if (isBogonIP(joinRequest.nodeInfo.ip)) {
      warn('addJoinRequest: This archiver join request uses a bogon IP')
      return { success: false, reason: 'This archiver join request is a bogon IP' }
    }
  }

  if (archivers.size > 0) {
    // Check the archiver version from dapp
    if (Context.config.p2p.validateArchiverAppData) {
      const validationResponse = validateArchiverAppData(joinRequest)
      if (validationResponse && !validationResponse.success) return validationResponse
    }

    // Check if the archiver request timestamp is within the acceptable timestamp range (after current cycle, before next cycle)
    const requestTimestamp = joinRequest.requestTimestamp
    const cycleDuration = newest.duration
    const cycleStart = newest.start
    const currentCycleStartTime = (cycleStart + cycleDuration) * 1000
    const nextCycleStartTime = (cycleStart + 2 * cycleDuration) * 1000

    if (requestTimestamp < currentCycleStartTime) {
      warn('addJoinRequest: This archiver join request timestamp is earlier than acceptable timestamp range')
      return {
        success: false,
        reason: 'This archiver join request timestamp is earlier than acceptable timestamp range',
      }
    }
    if (requestTimestamp > nextCycleStartTime) {
      warn('addJoinRequest: This archiver join request timestamp exceeds acceptable timestamp range')
      return {
        success: false,
        reason: 'This archiver join request timestamp exceeds acceptable timestamp range',
      }
    }

    // Get the consensus radius of the network
    try {
      const {
        shardGlobals: { consensusRadius },
      } = Context.stateManager.getCurrentCycleShardData()
      if (archivers.size >= consensusRadius *