Halborn Logo

Off-chain (Bridge) Full Re-Assessment - RuneMine


Prepared by:

Halborn Logo

HALBORN

Last Updated 09/04/2024

Date of Engagement by: June 24th, 2024 - August 1st, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

19

Critical

3

High

1

Medium

1

Low

7

Informational

7


1. Introduction

Runemine engaged Halborn to conduct a security audit of Runemine's bridge implementation, involving code review and audit techniques. The assessment began on June 24th, 2024 and ended on August 1st, 2024. The security assessment was scoped to the private GitHub repository provided to the Halborn team.

2. Assessment Summary

The team at Halborn was originally provided two weeks for the engagement and assigned two full-time security engineers to audit the security of all the repositories in scope. The time frame was further extended to six weeks due to code changes, new bridge deployments and UI changes. The engineers are security experts with advanced knowledge in penetration testing, red teaming, and multiple blockchain protocols.

The purpose of this audit was to:

- Perform automated and manual static analysis on the source code.
- Ensure that coding best practices are being followed by Runemine.
- Identify potential security issues within the source code and the bridge's operations.

Halborn initially conducted source code analysis of the commit ID f06c6f59bff58ce2d98220a7b290fdc7e74c287a. Runemine then requested to include newer code released as part of commit ID 22be772104892d1647d8c7a4163d41c9a846ccd8.

3. Test Approach and Methodology

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow security best practices.

The testers had recurrent difficulties deploying the bridge locally, using the public UI provided by Runemine on two separate URLs, and generally making transactions. While the original UI and bridge running at https://app-testnet.runemine.com/ worked, code changes pushed while testing rendered the bridge unusable. Runemine therefore provided a stable release with unit tests as part of commit ID 22be772104892d1647d8c7a4163d41c9a846ccd8. The final commit ID, however, contained a different code base with large changes compared to the original commit to audit. This code was therefore not reviewed as thoroughly, as tests were rather focused on creating bogus transactions to identify any improper behavior of the bridge.

4. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

5. SCOPE

Files and Repository
(a) Repository: bridge
(b) Assessed Commit ID: f06c6f5
(c) Items in scope:
  • bridge/bridge/bridge.go
  • bridge/bridge/repositories.go
  • bridge/bridge/types.go
↓ Expand ↓
Out-of-Scope:
Files and Repository
(a) Repository: bridge
(b) Assessed Commit ID: 58988ba
(c) Items in scope:
    Out-of-Scope:
    Remediation Commit ID:
    Out-of-Scope: New features/implementations after the remediation commit IDs.

    6. Assessment Summary & Findings Overview

    Critical

    3

    High

    1

    Medium

    1

    Low

    7

    Informational

    7

    Impact x Likelihood

    HAL-04

    HAL-05

    HAL-23

    HAL-06

    HAL-10

    HAL-17

    HAL-19

    HAL-11

    HAL-14

    HAL-15

    HAL-01

    HAL-03

    HAL-12

    HAL-13

    HAL-16

    HAL-18

    HAL-20

    HAL-21

    HAL-02

    Security analysisRisk levelRemediation Date
    Integer Overflow in UTXO AccumulationCriticalSolved - 08/23/2024
    Integer Overflow on the Fee CalculationCriticalSolved - 08/23/2024
    Decimals Not Ignored in Amount CalculationCriticalSolved - 08/23/2024
    Solana Poller Uses Panic, Risking Bridge HaltHighRisk Accepted
    Implement Timeout Handling in HTTP Utility FunctionsMediumSolved - 08/23/2024
    Deterministic Seed in Bridge ApplicationLowSolved - 08/23/2024
    Implement Secure Key Management SystemLowRisk Accepted
    Insecure Server ConfigurationLowRisk Accepted
    Missing nLockTime fieldLowSolved - 08/23/2024
    Lack of Backup RPC Endpoints for Bitcoin and SolanaLowRisk Accepted
    Outdated DependenciesLowSolved - 08/23/2024
    Ineffective Block ValidationLowSolved - 08/23/2024
    Implement Minimum Transfer Amount Check in Bridge FunctionInformationalAcknowledged
    Standardize Logging Across the Bridge ImplementationInformationalAcknowledged
    Potential Time Synchronization Issue in Bridge TransactionsInformationalAcknowledged
    Weak Seed Generation ProcessInformationalAcknowledged
    Potential Zero Fee for Small TransfersInformationalAcknowledged
    Overly Permissive CORS ConfigurationInformationalAcknowledged
    Missing Check on Bitcoin OpcodeInformationalAcknowledged

    7. Findings & Tech Details

    7.1 Integer Overflow in UTXO Accumulation

    // Critical

    Description

    There's a risk of integer overflow in the UTXO accumulation process within the appendUtxos function. The current implementation uses int64 for inputSatoshis, which could overflow when dealing with large or numerous UTXOs.

     int64 total amount of input utxos
     */
    func (a *Adapter) appendUtxos(tx *wire.MsgTx, receiver string, runeID string, runeAmount string) ([]int64, error) {
    	if runeAmount == "0" {
    		return nil, fmt.Errorf("runeAmount must be greater than 0")
    	}
    
    	var res struct{ FastestFee int64 }
    	err := util.Http(&res, "GET", "https://mempool.space/api/v1/fees/recommended", "", nil)
    	if err != nil {
    		return nil, err
    	}
    
    	// add payment utxos
    	fee := res.FastestFee * 150 / 100
    	if fee > maxFeePerByte {
    		fee = maxFeePerByte
    	}
    	costSatoshis := int64(runeUtxoValue + runeUtxoValue + (fee * bytesPerRuneUtxo))
    	inputSatoshis := int64(0)
    	inputAmounts := []int64{}
    
    	utxos, err := a.client.Utxos(a.multisig)
    	if err != nil {
    		return nil, err
    	}
    	sort.Slice(utxos, func(i, j int) bool {
    		if utxos[i].Status.Confirmed != utxos[i].Status.Confirmed {
    			return utxos[i].Status.Confirmed
    		}
    		return utxos[i].Value > utxos[j].Value
    	})
    
    	for _, utxo := range utxos {
    		value := int64(utxo.Value)
    		txId := utxo.Txid
    		voutIdx := uint32(utxo.Vout)
    
    		// don't spend our "dust" runes utxos
    		// exist with more than 10k satoshis
    		if value <= minimumDustThreshold {
    			continue
    		}
    
    		// skip recently spent utxo (that the utxo api might not have indexed)
    		if isSpent(fmt.Sprintf("%s:%d", txId, voutIdx)) {
    			continue
    		}
    
    		inputSatoshis += value
    		inputAmounts = append(inputAmounts, value)
    
    		hash, err := chainhash.NewHashFromStr(txId)
    		if err != nil {
    			return nil, err
    		}
    
    		tx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(hash, voutIdx), nil, nil))
    		if inputSatoshis > costSatoshis {
    			break
    		}
    	}
    
    	if inputSatoshis < costSatoshis {
    		return nil, fmt.Errorf("not enough utxos to cover cost: %d < %d", inputSatoshis, costSatoshis)
    	}
    
    	changeSatoshis := inputSatoshis - costSatoshis
    
    	// add runes utxos
    	runesUtxos, err := a.indexer.GetUtxos(a.multisig)
    	if err != nil {
    		return nil, err
    	}
    	targetAmount := util.StringToInt(runeAmount)
    	currentAmount := int64(0)
    	for _, u := range runesUtxos.Utxos {
    		idParts := strings.Split(u.Id, ":")
    		index := uint32(util.StringToInt(idParts[1]))
    		// skip recently spent utxo (that the utxo api might not have indexed)
    		if isSpent(fmt.Sprintf("%s:%d", idParts[0], index)) {
    			continue
    		}
    
    		b, ok := u.Balances[runeID]
    		if !ok {
    			continue
    		}
    		amount, err := b.Int64()
    		if err != nil {
    			continue
    		}
    		currentAmount += amount
    
    		inputAmounts = append(inputAmounts, int64(u.Value))
    		hash, err := chainhash.NewHashFromStr(idParts[0])
    		if err != nil {
    			return nil, err
    		}
    
    		tx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(hash, index), nil, nil))
    		if currentAmount >= targetAmount {
    			break
    		}
    	}
    
    	if currentAmount < targetAmount {
    		return nil, fmt.Errorf("not enough utxos (RUNES) to cover cost: %d < %d", currentAmount, targetAmount)
    	}
    
    	// add outputs - it should be extracted into a separate function
    
    	for _, out := range []struct {
    		Address string
    		Amount  int64
    	}{{a.multisig, runeUtxoValue}, {receiver, runeUtxoValue}, {a.multisig, changeSatoshis}} {
    		outputAddress, err := btcutil.DecodeAddress(out.Address, a.net)
    		if err != nil {
    			return nil, err
    		}
    		pkScript, err := txscript.PayToAddrScript(outputAddress)
    		if err != nil {
    			return nil, err
    		}
    		tx.AddTxOut(wire.NewTxOut(out.Amount, pkScript))
    	}
    
    	split := strings.Split(runeID, ".")
    
    	runestoneInts := []*big.Int{big.NewInt(0), util.StringToBi(split[0]), util.StringToBi(split[1]), util.StringToBi(runeAmount), big.NewInt(1)}
    	runestoneBytes := []byte{}
    	zero := big.NewInt(0)
    	for _, i := range runestoneInts {
    		for (&big.Int{}).Rsh(i, 7).Cmp(zero) > 0 {
    			b := i.Bytes()
    			runestoneBytes = append(runestoneBytes, b[len(b)-1]|0b1000_0000)
    			i.Rsh(i, 7)
    		}
    		b := i.Bytes()
    		if len(b) > 0 {
    			runestoneBytes = append(runestoneBytes, b[len(b)-1])
    		} else {
    			runestoneBytes = append(runestoneBytes, 0)
    		}
    	}
    	runestoneBytes = append([]byte{106, 93, byte(len(runestoneBytes))}, runestoneBytes...)
    	tx.AddTxOut(&wire.TxOut{PkScript: runestoneBytes})
    
    	return inputAmounts, nil
    }

    Proof of Concept
    package main
    
    import (
      "fmt"
      "math"
    )
    
    func main() {
      // Simulate the UTXO accumulation process
      inputSatoshis := int64(0)
      
      // Let's use UTXOs that will cause an overflow
      utxos := []int64{
        math.MaxInt64 - 10, // Very close to max int64
        20,                 // This will cause the overflow
      }
    
      fmt.Println("Initial inputSatoshis:", inputSatoshis)
    
      for i, value := range utxos {
        prevInputSatoshis := inputSatoshis
        inputSatoshis += value
        
        fmt.Printf("After UTXO %d (value %d):\n", i+1, value)
        fmt.Printf("  Previous total: %d\n", prevInputSatoshis)
        fmt.Printf("  New total: %d\n", inputSatoshis)
        
        if inputSatoshis < prevInputSatoshis {
          fmt.Printf("OVERFLOW DETECTED! The total decreased after adding a positive value.\n")
        }
        
        fmt.Println()
      }
    }


    BVSS
    Recommendation

    Use big.Int for UTXO value accumulation to prevent overflow.

    Remediation

    SOLVED: The RuneMine team solved the issue by implementing the suggested recommendations.

    Remediation Hash
    References

    7.2 Integer Overflow on the Fee Calculation

    // Critical

    Description

    There is an integer overflow vulnerability in the appendUtxos function of the Bitcoin adapter. This issue arises from the calculation of the transaction fee, which could lead to unexpected behavior, including the creation of transactions with incorrect fees or the inability to create transactions at all.


     int64 total amount of input utxos
     */
    func (a *Adapter) appendUtxos(tx *wire.MsgTx, receiver string, runeID string, runeAmount string) ([]int64, error) {
    	if runeAmount == "0" {
    		return nil, fmt.Errorf("runeAmount must be greater than 0")
    	}
    
    	var res struct{ FastestFee int64 }
    	err := util.Http(&res, "GET", "https://mempool.space/api/v1/fees/recommended", "", nil)
    	if err != nil {
    		return nil, err
    	}
    
    	// add payment utxos
    	fee := res.FastestFee * 150 / 100
    	if fee > maxFeePerByte {
    		fee = maxFeePerByte
    	}
    	costSatoshis := int64(runeUtxoValue + runeUtxoValue + (fee * bytesPerRuneUtxo))
    	inputSatoshis := int64(0)
    	inputAmounts := []int64{}
    
    	utxos, err := a.client.Utxos(a.multisig)
    	if err != nil {
    		return nil, err
    	}
    	sort.Slice(utxos, func(i, j int) bool {
    		if utxos[i].Status.Confirmed != utxos[i].Status.Confirmed {
    			return utxos[i].Status.Confirmed
    		}
    		return utxos[i].Value > utxos[j].Value
    	})
    
    	for _, utxo := range utxos {
    		value := int64(utxo.Value)
    		txId := utxo.Txid
    		voutIdx := uint32(utxo.Vout)
    
    		// don't spend our "dust" runes utxos
    		// exist with more than 10k satoshis
    		if value <= minimumDustThreshold {
    			continue
    		}
    
    		// skip recently spent utxo (that the utxo api might not have indexed)
    		if isSpent(fmt.Sprintf("%s:%d", txId, voutIdx)) {
    			continue
    		}
    
    		inputSatoshis += value
    		inputAmounts = append(inputAmounts, value)
    
    		hash, err := chainhash.NewHashFromStr(txId)
    		if err != nil {
    			return nil, err
    		}
    
    		tx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(hash, voutIdx), nil, nil))
    		if inputSatoshis > costSatoshis {
    			break
    		}
    	}
    
    	if inputSatoshis < costSatoshis {
    		return nil, fmt.Errorf("not enough utxos to cover cost: %d < %d", inputSatoshis, costSatoshis)
    	}
    
    	changeSatoshis := inputSatoshis - costSatoshis
    
    	// add runes utxos
    	runesUtxos, err := a.indexer.GetUtxos(a.multisig)
    	if err != nil {
    		return nil, err
    	}
    	targetAmount := util.StringToInt(runeAmount)
    	currentAmount := int64(0)
    	for _, u := range runesUtxos.Utxos {
    		idParts := strings.Split(u.Id, ":")
    		index := uint32(util.StringToInt(idParts[1]))
    		// skip recently spent utxo (that the utxo api might not have indexed)
    		if isSpent(fmt.Sprintf("%s:%d", idParts[0], index)) {
    			continue
    		}
    
    		b, ok := u.Balances[runeID]
    		if !ok {
    			continue
    		}
    		amount, err := b.Int64()
    		if err != nil {
    			continue
    		}
    		currentAmount += amount
    
    		inputAmounts = append(inputAmounts, int64(u.Value))
    		hash, err := chainhash.NewHashFromStr(idParts[0])
    		if err != nil {
    			return nil, err
    		}
    
    		tx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(hash, index), nil, nil))
    		if currentAmount >= targetAmount {
    			break
    		}
    	}
    
    	if currentAmount < targetAmount {
    		return nil, fmt.Errorf("not enough utxos (RUNES) to cover cost: %d < %d", currentAmount, targetAmount)
    	}
    
    	// add outputs - it should be extracted into a separate function
    
    	for _, out := range []struct {
    		Address string
    		Amount  int64
    	}{{a.multisig, runeUtxoValue}, {receiver, runeUtxoValue}, {a.multisig, changeSatoshis}} {
    		outputAddress, err := btcutil.DecodeAddress(out.Address, a.net)
    		if err != nil {
    			return nil, err
    		}
    		pkScript, err := txscript.PayToAddrScript(outputAddress)
    		if err != nil {
    			return nil, err
    		}
    		tx.AddTxOut(wire.NewTxOut(out.Amount, pkScript))
    	}
    
    	split := strings.Split(runeID, ".")
    
    	runestoneInts := []*big.Int{big.NewInt(0), util.StringToBi(split[0]), util.StringToBi(split[1]), util.StringToBi(runeAmount), big.NewInt(1)}
    	runestoneBytes := []byte{}
    	zero := big.NewInt(0)
    	for _, i := range runestoneInts {
    		for (&big.Int{}).Rsh(i, 7).Cmp(zero) > 0 {
    			b := i.Bytes()
    			runestoneBytes = append(runestoneBytes, b[len(b)-1]|0b1000_0000)
    			i.Rsh(i, 7)
    		}
    		b := i.Bytes()
    		if len(b) > 0 {
    			runestoneBytes = append(runestoneBytes, b[len(b)-1])
    		} else {
    			runestoneBytes = append(runestoneBytes, 0)
    		}
    	}
    	runestoneBytes = append([]byte{106, 93, byte(len(runestoneBytes))}, runestoneBytes...)
    	tx.AddTxOut(&wire.TxOut{PkScript: runestoneBytes})
    
    	return inputAmounts, nil
    }
    
    
    BVSS
    Recommendation

    Use big.Int for fee calculations to avoid overflow issues.

    Remediation

    SOLVED: The RuneMine team solved the issue by implementing the suggested recommendations.

    Remediation Hash
    References

    7.3 Decimals Not Ignored in Amount Calculation

    // Critical

    Description

    The current implementation does not properly handle decimal places in amount calculations, potentially leading to incorrect fee calculations and transfer amounts.

    In the Bridge function of the Solana adapter, the amount calculation does not account for the decimal places of the token. This can result in incorrect fee calculations and transfer amounts, especially for tokens with different decimal places.


    fee := uint64(util.StringToInt(transfer.Amount)) * a.feePercent / 1_000
    amountWithoutFee := uint64(util.StringToInt(transfer.Amount)) - fee


    BVSS
    Recommendation

    Modify the amount calculation to account for these decimal places.

    Remediation

    SOLVED: The RuneMine team solved the issue by using created by them and their decimals match the runes ones.

    References

    7.4 Solana Poller Uses Panic, Risking Bridge Halt

    // High

    Description

    The current implementation of the Solana poller uses panic for error handling in several places. This approach can cause the entire bridge process to halt when a single error occurs.

    Problematic Code Example

    1. In the poll function:


    util.Check(err) // This function panics on error

    2. Direct use of panic:


    if fee < 50000000 {
    panic(fmt.Sprintf("sol fee paid to small to cover transaction cost: %d", fee))
    }



    3. In utility functions:


    func Check(err error) {
    if err != nil {
    panic(err)
    }
    }

    BVSS
    Recommendation

    There's no visible panic recovery mechanism in the main function to catch panics from goroutines. This could lead to goroutine leaks and partial system failure.

    Remediation

    RISK ACCEPTED: The RuneMine team accepted the risk of the issue.

    7.5 Implement Timeout Handling in HTTP Utility Functions

    // Medium

    Description

    The current HTTP utility functions (Http and HttpVal) do not implement any timeout mechanism. This could lead to hanging requests if the server doesn't respond.

    Current behavior:
    - HTTP requests are made using the default HTTP client without any timeout setting.
    - Long-running or non-responsive requests could block indefinitely.

    BVSS
    Recommendation

    1. Implement a configurable timeout for HTTP requests.
    2. Use http.Client with a timeout instead of http.DefaultClient.
    3. Add context support for cancellation and timeouts.

    Reference : https://pkg.go.dev/net/http

    Remediation

    SOLVED: The RuneMine team solved the issue by implementing the suggested recommendations.

    Remediation Hash
    References

    7.6 Deterministic Seed in Bridge Application

    // Low

    Description

    The bridge application uses a time-based seeding method for its random number generator:


    rand.Seed(uint64(time.Now().UnixNano()))
    BVSS
    Recommendation

    Implement a cryptographically secure random number generator. Consider using `crypto/rand` from the Go standard library:

    seed, _ := crypto/rand.Int(crypto/rand.Reader, big.NewInt(1<<63-1))
    rand.Seed(seed.Uint64())
    Remediation

    SOLVED: The RuneMine team solved the issue by implementing the suggested recommendations.

    Remediation Hash
    References

    7.7 Implement Secure Key Management System

    // Low

    Description

    The current implementation stores sensitive private keys as environment variables:


    BitcoinPrivateKeys: util.EnvArray("BITCOIN_PRIVATE_KEYS"),
    SolanaPrivateKeys:  util.EnvArray("SOLANA_PRIVATE_KEYS"),

    This approach poses security risks, as environment variables can be exposed through system logs, debugging output, or compromised system access.


    BVSS
    Recommendation

    Implement a secure key management system:

    1. Use a dedicated key management service (e.g., HashiCorp Vault, AWS KMS, or Azure Key Vault).

    2. Encrypt keys at rest and in transit.

    3. Implement proper access controls and auditing for key operations.

    4. Use temporary, rotating credentials instead of long-lived keys where possible.

    Remediation

    RISK ACCEPTED: The RuneMine team accepted the risk of the issue.

    References

    7.8 Insecure Server Configuration

    // Low

    Description

    The current server configuration uses HTTP instead of HTTPS, as evidenced by the following line of code:

    http.ListenAndServe(":"+port, nil)
    BVSS
    Recommendation

    Implement HTTPS by using TLS/SSL. This involves the following steps:

    1. Obtain an SSL/TLS certificate from a trusted Certificate Authority (CA).
    2. Configure the server to use HTTPS instead of HTTP.
    3. Implement proper TLS settings to ensure strong encryption and security.

    Remediation

    RISK ACCEPTED: The RuneMine team accepted the risk of the issue.

    References

    7.9 Missing nLockTime field

    // Low

    Description

    The current implementation of Bitcoin transactions in the bridge doesn't set the nLockTime field. This could potentially expose our transactions to fee sniping attacks during blockchain reorganizations.

    BVSS
    Recommendation

    Consider using nLockTime field.

    Remediation

    SOLVED: The RuneMine team solved the issue by implementing the suggested recommendations.

    Remediation Hash

    7.10 Lack of Backup RPC Endpoints for Bitcoin and Solana

    // Low

    Description

    The current configuration uses single RPC endpoints for both Bitcoin and Solana networks without any backup options. This creates a single point of failure that could lead to service disruptions.

    BVSS
    Recommendation

    1. Implement RPC redundancy:

    - Add multiple RPC endpoints for both Bitcoin and Solana networks

    - Implement a failover mechanism to switch between RPCs

    - Consider using a load balancing approach to distribute requests across multiple RPCs

    2. Enhance configuration management:

    - Move RPC URLs to a configuration file or environment variables

    - Allow easy addition and removal of RPC endpoints without code changes

    3. Implement health checks and monitoring:

    - Regularly check the health of all RPC endpoints

    - Automatically switch to healthy RPCs when issues are detected

    - Alert the operations team when RPC issues are encountered

    Remediation

    RISK ACCEPTED: The RuneMine team accepted the risk of the issue.

    References

    7.11 Outdated Dependencies

    // Low

    Description

    Multiple outdated dependencies were found in the codebase. These dependencies include versions with known security vulnerabilities that pose significant risks to the application’s security and stability.

    Proof of Concept
    +-----------------------------------------------------+----------+-------------+--------+------------------+
    |                       MODULE                        | VERSION  | NEW VERSION | DIRECT | VALID TIMESTAMPS |
    +-----------------------------------------------------+----------+-------------+--------+------------------+
    | github.com/aws/aws-sdk-go-v2                        | v1.27.2  | v1.30.0     | true   | true             |
    | github.com/aws/aws-sdk-go-v2/config                 | v1.27.18 | v1.27.22    | true   | true             |
    | github.com/aws/aws-sdk-go-v2/service/secretsmanager | v1.30.0  | v1.32.0     | true   | true             |
    | github.com/btcsuite/btcd                            | v0.24.0  | v0.24.2     | true   | true             |
    | github.com/gagliardetto/solana-go                   | v1.10.0  | v1.11.0     | true   | true             |
    | github.com/jackc/pgx/v5                             | v5.5.5   | v5.6.0      | true   | true             |
    | gorm.io/driver/postgres                             | v1.5.7   | v1.5.9      | true   | true             |
    | gorm.io/gorm                                        | v1.25.9  | v1.25.10    | true   | true             |
    +-----------------------------------------------------+----------+-------------+--------+------------------+
    Score
    Impact: 2
    Likelihood: 2
    Recommendation

    Update all affected dependencies to the latest possible version.

    Remediation

    SOLVED: The RuneMine team solved the issue by upgrading dependencies.

    Remediation Hash

    7.12 Ineffective Block Validation

    // Low

    Description

    Bitcoin block validation ensures that every block added to the blockchain is legitimate and maintains the integrity of the network. It involves verifying the block header, ensuring proof of work, validating all transactions, checking the block reward, and ensuring the block integrates correctly into the blockchain. This process is fundamental to the security and functionality of Bitcoin.

    Sometimes, temporary forks or competing chains can occur. A fork happens when two miners find a block at the same time, leading to two versions of the blockchain. The network will eventually adopt the longest chain (with the most proof of work), potentially abandoning blocks from shorter chains.

    Time based validations cannot be considered safe as they are not validating the number of blocks confirmed.

    Proof of Concept
    func (p *Poller) poll(ctx context.Context, transfers chan<- *bridge.Transfer, errors chan<- error) {
    	response, err := p.indexer.GetTransfersByDestination(p.bridgeMultisig)
    	if err != nil {
    		errors <- err
    		return
    	}
    	for _, t := range response.Transfers {
    		tx, err := p.indexer.GetTransaction(t.Tx)
    		if err != nil {
    			errors <- err
    			continue
    		}
    
    		// to handle reorgs, wait for tx to be 20m past 1st confirmation
    		if time.Now().Unix()-t.Time < 20*60 {
    			continue
    		}
    Score
    Impact: 2
    Likelihood: 2
    Recommendation

    Monitor the number of confirmations of a transaction to avoid potentials reorganizations, so that triggering operations like minting or burn before the transactions is fully ensured only afterwards.

    Remediation

    SOLVED: The RuneMine team solved the issue by increasing confirmation numbers, and they are going to monitor it.

    Remediation Hash

    7.13 Implement Minimum Transfer Amount Check in Bridge Function

    // Informational

    Description

    The current bridge function in the Bridge struct lacks a minimum transfer amount check. While this check is implemented in the BTC and Solana pollers, adding it to the central bridge function would provide an additional layer of security and consistency.


    package bridge
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"runemine-bridge/util"
    	"slices"
    	"strconv"
    	"time"
    
    	"gorm.io/gorm"
    )
    
    type Bridge struct {
    	repository     *TransferRepository
    	runesWhitelist []string
    	Pollers        map[int]Poller
    	Adapters       map[int]Adapter
    }
    
    func NewBridge(config *Config, db *gorm.DB) *Bridge {
    	return &Bridge{
    		repository:     NewTransferRepository(db),
    		runesWhitelist: config.RunesWhitelist,
    		Pollers:        map[int]Poller{},
    		Adapters:       map[int]Adapter{},
    	}
    }
    
    func (b *Bridge) SetPoller(network int, p Poller) {
    	b.Pollers[network] = p
    }
    
    func (b *Bridge) SetAdapter(network int, p Adapter) {
    	b.Adapters[network] = p
    }
    
    func (b *Bridge) Run(ctx context.Context) chan error {
    	errorChannel := make(chan error)
    	for network, p := range b.Pollers {
    		go b.run(ctx, network, p, errorChannel)
    	}
    	return errorChannel
    }
    
    func (b *Bridge) run(ctx context.Context, network int, p Poller, errorChannel chan<- error) {
    	transferChannel := make(chan *Transfer, 10)
    	defer close(transferChannel)
    	go p.Poll(ctx, transferChannel, errorChannel)
    	for {
    		select {
    		case transfer := <-transferChannel:
    			b.bridge(ctx, network, transfer)
    		case <-ctx.Done():
    			return
    		}
    	}
    }
    
    func (b *Bridge) bridge(ctx context.Context, network int, transfer *Transfer) {
    	defer func() {
    		if err := recover(); err != nil {
    			transfer.Updated = time.Now()
    			transfer.Status = "error"
    			transfer.Error = fmt.Sprintf("panic: %v", err)
    			log.Println("bridge panic", transfer.ID, transfer.Error)
    			util.Check(b.repository.Save(transfer))
    		}
    	}()
    
    	_, err := b.repository.FindById(transfer.ID)
    	if err == nil {
    		//log.Println("existing", transfer.ID)
    		return
    	}
    
    	log.Println("bridging", transfer.ID)
    	transfer.Source = network
    	transfer.Created = time.Now()
    	transfer.Updated = transfer.Created
    	if transfer.Status == "error" {
    		log.Println("bridge error", transfer.ID, transfer.Error)
    		util.Check(b.repository.Save(transfer))
    		return
    	}
    
    	if !slices.Contains(b.runesWhitelist, transfer.Rune) {
    		transfer.Status = "error"
    		transfer.Error = "rune not whitelisted"
    		log.Println("bridge error", transfer.ID, transfer.Error)
    		util.Check(b.repository.Save(transfer))
    		return
    	}
    
    	if transfer.To == "" {
    		transfer.Status = "error"
    		transfer.Error = "missing to wallet"
    		log.Println("bridge error", transfer.ID, transfer.Error)
    		util.Check(b.repository.Save(transfer))
    		return
    	}
    
    	if transfer.Amount == "" || transfer.Amount == "0" {
    		transfer.Status = "error"
    		transfer.Error = "missing amount"
    		log.Println("bridge error", transfer.ID, transfer.Error)
    		util.Check(b.repository.Save(transfer))
    		return
    	}
    
    	a, ok := b.Adapters[transfer.Target]
    	if !ok {
    		transfer.Status = "error"
    		transfer.Error = "unsupported network " + strconv.Itoa(transfer.Target)
    		log.Println("bridge error", transfer.ID, transfer.Error)
    		util.Check(b.repository.Save(transfer))
    		return
    	}
    
    	// Save now to avoid calling adapter.Bridge twice
    	transfer.Status = "sending"
    	util.Check(b.repository.Save(transfer))
    
    	targetId, err := a.Bridge(ctx, transfer)
    	transfer.Status = "sent"
    	transfer.Updated = time.Now()
    	transfer.TargetID = targetId
    	if err != nil {
    		transfer.Status = "error"
    		transfer.Error = err.Error()
    		log.Println("bridge error", transfer.ID, transfer.Error)
    	}
    	util.Check(b.repository.Save(transfer))
    	log.Println("bridged", transfer.ID, transfer.TargetID)
    }
    Score
    Impact:
    Likelihood:
    Recommendation

    Implement a minimum transfer amount check in the bridge function:

    1. Add a minimumTransferAmount field to the Bridge struct or Config.

    Remediation

    ACKNOWLEDGED: The RuneMine team acknowledged the issue.

    References

    7.14 Standardize Logging Across the Bridge Implementation

    // Informational

    Description

    The current implementation uses a mix of logging methods, including fmt.Println, log.Println, and direct panic calls. This inconsistency makes it difficult to manage log levels and format logs uniformly across the bridge system.

    Current issues:
    1. Inconsistent use of logging methods (fmt.Println, log.Println, panic).
    2. Lack of log levels (DEBUG, INFO, ERROR, etc.).
    3. No centralized control over log output and formatting.

    Score
    Impact:
    Likelihood:
    Recommendation

    Implement a standardized logging system across the bridge, with the following features:
    1. Define log levels (DEBUG, INFO, WARN, ERROR, FATAL).
    2. Create a centralized logging package or utility.
    3. Replace all current logging calls with the new standardized logger.

    Remediation

    ACKNOWLEDGED: The RuneMine team acknowledged the issue.

    7.15 Potential Time Synchronization Issue in Bridge Transactions

    // Informational

    Description

    The Bridge function in the provided code uses time.Now() to set the Created and Updated fields of transactions. This approach can lead to inconsistencies in transaction timestamps across different nodes or systems, especially in a distributed environment.

    Score
    Impact:
    Likelihood:
    Recommendation

    Consider using Network Time Protocol (NTP) to ensure all nodes are synchronized to a common time source.

    Remediation

    ACKNOWLEDGED: The RuneMine team acknowledged the issue.

    7.16 Weak Seed Generation Process

    // Informational

    Description

    The current seed generation process in the generateKey function potentially uses a weak source of entropy, which could compromise the security of the generated keys.

    func generateKey(i int) (string, []byte) {
        if mnemonic := env("SEED"+strconv.Itoa(i), ""); mnemonic != "" {
            seed, err := bip39.MnemonicToSeed(mnemonic, "")
            check(err)
            return mnemonic, seed
        }
        entropy, err := bip39.GenerateEntropy(bip39.EntWords12)
        check(err)
        mnemonic, seed, err := bip39.Mnemonic(entropy, "")
        check(err)
        return mnemonic, seed
    }
    Score
    Impact:
    Likelihood:
    Recommendation

    1. Use a cryptographically secure random number generator (CSPRNG) explicitly, such as crypto/rand from the Go standard library.
    2. Avoid using environment variables for seed storage. If needed, use a secure key management system.
    3. Increase the entropy to 256 bits (24 words) for enhanced security.
    4. Add a strong passphrase to the seed generation process.

    Remediation

    ACKNOWLEDGED: The RuneMine team acknowledged the issue.

    References

    7.17 Potential Zero Fee for Small Transfers

    // Informational

    Description

    In the `Bridge` function, the fee calculation uses the following formula:


    fee := uint64(util.StringToInt(transfer.Amount)) * a.feePercent / 1_000

    For very small transfer amounts, this could result in a fee of 0 due to integer division. This may allow free transactions, which could be exploited and may not be economically viable for the system.


    Score
    Impact:
    Likelihood:
    Recommendation

    1. Implement a minimum fee threshold.

    2. Use fixed-point arithmetic for more precise fee calculations.

    3. Round up fees to the nearest whole number, ensuring a minimum of 1.

    Remediation

    ACKNOWLEDGED: The RuneMine team acknowledged the issue.

    References

    7.18 Overly Permissive CORS Configuration

    // Informational

    Description

    The server implements an overly permissive CORS policy:


    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Access-Control-Allow-Methods", "HEAD, GET, POST")
    w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization")
    Score
    Impact:
    Likelihood:
    Recommendation

    1. Restrict the `Access-Control-Allow-Origin` header to specific, trusted domains.

    2. Implement a whitelist of allowed origins.

    3. Consider using environment variables to configure allowed origins for different environments.

    Remediation

    ACKNOWLEDGED: The RuneMine team acknowledged the issue.

    References

    7.19 Missing Check on Bitcoin Opcode

    // Informational

    Description

    When sending a transaction with a different Bitcoin Opcode, the bridge processed the transaction and did not verify that the OP_RETURN opcode was used. This eventually resulted in the user's wallet being locked. Subsequent transactions failed along with the error bad-txns-inputs-missingorsend.

    Proof of Concept

    The Bitcoin Opcode was changed from 106 (OP_RETURN) to 97 (NOP).

    Transaction with incorrect Opcode.Subsequent transaction and error.
    Score
    Impact: 1
    Likelihood: 1
    Recommendation

    The bridge could ensure that the correct Bitcoin Opcode is used in transactions, namely 106 - OP_RETURN.

    Remediation

    ACKNOWLEDGED: The RuneMine team acknowledged the issue.

    Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

    © Halborn 2024. All rights reserved.