DocumentationVulnerability Types

Solana Vulnerability Types

Common security vulnerabilities detected by Believe Security

Believe Security detects a wide range of Solana-specific vulnerabilities, as well as general program security issues. This page documents the most common types of vulnerabilities that our system can identify, grouped by severity level.

Critical Vulnerabilities

Missing Signer Verification

A critical vulnerability where a program fails to verify that a transaction is signed by the appropriate authority, allowing unauthorized access to protected operations.

// Vulnerable code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let authority = next_account_info(account_info_iter)?;
    let vault = next_account_info(account_info_iter)?;
    
    // VULNERABILITY: No verification that authority is a signer
    // transfer_funds(authority, vault)?;
    
    Ok(())
}
// Fixed code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let authority = next_account_info(account_info_iter)?;
    let vault = next_account_info(account_info_iter)?;
    
    // Verify that the authority is a signer
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    
    // transfer_funds(authority, vault)?;
    
    Ok(())
}

Improper Account Validation

A vulnerability where a program does not properly validate the ownership or expected structure of accounts, allowing attackers to pass malicious accounts.

// Vulnerable code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let token_account = next_account_info(account_info_iter)?;
    
    // VULNERABILITY: No validation of token_account ownership or type
    // let token_data = TokenAccount::unpack(&token_account.data.borrow())?;
    
    Ok(())
}
// Fixed code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let token_account = next_account_info(account_info_iter)?;
    
    // Verify token program ownership
    if token_account.owner != &spl_token::id() {
        return Err(ProgramError::InvalidAccountOwner);
    }
    
    // Validate token account structure
    // let token_data = TokenAccount::unpack(&token_account.data.borrow())?;
    
    Ok(())
}

High Severity Vulnerabilities

Unchecked Owner in CPI Calls

Occurs when a program makes a Cross-Program Invocation (CPI) without verifying the called program's ID, potentially allowing attackers to substitute malicious programs.

// Vulnerable code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;
    
    // VULNERABILITY: No verification of token_program's ID
    invoke(
        &transfer_instruction(/* params */),
        &[/* accounts */],
    )?;
    
    Ok(())
}
// Fixed code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let user = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;
    
    // Verify token program ID
    if token_program.key != &spl_token::id() {
        return Err(ProgramError::IncorrectProgramId);
    }
    
    invoke(
        &transfer_instruction(/* params */),
        &[/* accounts */],
    )?;
    
    Ok(())
}

Integer Overflow/Underflow

Occurs when arithmetic operations are performed without proper bounds checking, potentially leading to unexpected program behavior or fund loss.

// Vulnerable code
fn transfer_tokens(
    amount: u64,
    sender_balance: u64,
    fee: u64,
) -> Result<u64, ProgramError> {
    // VULNERABILITY: Potential overflow if fee > amount
    let amount_after_fee = amount - fee;
    
    // VULNERABILITY: Potential underflow if sender_balance < amount_after_fee
    let new_balance = sender_balance - amount_after_fee;
    
    Ok(new_balance)
}
// Fixed code
fn transfer_tokens(
    amount: u64,
    sender_balance: u64,
    fee: u64,
) -> Result<u64, ProgramError> {
    // Check if fee is greater than amount
    if fee > amount {
        return Err(ProgramError::InvalidArgument);
    }
    
    let amount_after_fee = amount.checked_sub(fee)
        .ok_or(ProgramError::ArithmeticOverflow)?;
    
    // Check if sender has sufficient balance
    if sender_balance < amount_after_fee {
        return Err(ProgramError::InsufficientFunds);
    }
    
    let new_balance = sender_balance.checked_sub(amount_after_fee)
        .ok_or(ProgramError::ArithmeticOverflow)?;
    
    Ok(new_balance)
}

Medium Severity Vulnerabilities

Missing Rent Exemption Check

Occurs when a program does not verify that an account is rent-exempt, potentially allowing the account to be closed and its data lost due to insufficient funds.

// Vulnerable code
fn initialize_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let new_account = next_account_info(account_info_iter)?;
    
    // VULNERABILITY: No rent exemption check
    // Initialize account data...
    
    Ok(())
}
// Fixed code
fn initialize_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let new_account = next_account_info(account_info_iter)?;
    let rent = next_account_info(account_info_iter)?;
    
    // Verify rent exemption
    let rent = &Rent::from_account_info(rent)?;
    if !rent.is_exempt(new_account.lamports(), new_account.data_len()) {
        return Err(ProgramError::InsufficientFunds);
    }
    
    // Initialize account data...
    
    Ok(())
}

Unhandled Error Cases

Occurs when a program does not properly handle all possible error conditions, potentially leading to unexpected behavior or security issues.

// Vulnerable code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let account = next_account_info(account_info_iter)?;
    
    // VULNERABILITY: Unhandled error cases in external call
    let result = external_function();
    
    // Continue processing assuming success...
    
    Ok(())
}
// Fixed code
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let account = next_account_info(account_info_iter)?;
    
    // Properly handle all error cases
    let result = external_function()
        .map_err(|err| {
            // Map external errors to appropriate program errors
            match err {
                ExternalError::InvalidInput => ProgramError::InvalidArgument,
                ExternalError::InsufficientBalance => ProgramError::InsufficientFunds,
                _ => ProgramError::Custom(1), // General error code
            }
        })?;
    
    // Continue processing...
    
    Ok(())
}

Low Severity Vulnerabilities

Inefficient Data Validation

Occurs when a program performs unnecessary or inefficient validation of input data, potentially leading to higher compute unit consumption or complexity.

// Inefficient code
fn validate_data(data: &[u8]) -> ProgramResult {
    // INEFFICIENCY: Multiple iterations over the same data
    if data.len() == 0 {
        return Err(ProgramError::InvalidInstructionData);
    }
    
    for byte in data.iter() {
        // First validation...
    }
    
    for byte in data.iter() {
        // Second validation...
    }
    
    Ok(())
}
// Optimized code
fn validate_data(data: &[u8]) -> ProgramResult {
    // Check for empty data first
    if data.is_empty() {
        return Err(ProgramError::InvalidInstructionData);
    }
    
    // Single pass validation
    for byte in data.iter() {
        // First validation...
        
        // Second validation...
    }
    
    Ok(())
}

Missing Documentation

While not a direct security vulnerability, missing or insufficient documentation can lead to integration errors or misuse of the program.

// Poorly documented code
fn process_transfer(
    amount: u64,
    accounts: &[AccountInfo],
) -> ProgramResult {
    // No documentation about expected account order or requirements
    let source = &accounts[0];
    let destination = &accounts[1];
    
    // Transfer logic...
    
    Ok(())
}
/// Processes a token transfer between two accounts
///
/// # Arguments
///
/// * `amount` - The amount of tokens to transfer
/// * `accounts` - The accounts required for the transfer:
///   * accounts[0] - The source account (must be a token account owned by the program)
///   * accounts[1] - The destination token account
///   * accounts[2] - The authority that owns the source account (must be a signer)
///   * accounts[3] - The token program
///
/// # Errors
///
/// This function will return an error if:
/// * The authority is not a signer
/// * The source or destination accounts are invalid
/// * The source has insufficient balance
fn process_transfer(
    amount: u64,
    accounts: &[AccountInfo],
) -> ProgramResult {
    // Transfer logic with appropriate validation...
    
    Ok(())
}

Security Best Practices

In addition to identifying specific vulnerabilities, Believe Security provides recommendations for following security best practices:

  • Use checked arithmetic operations - Always use checked operations (like checked_add, checked_sub) instead of regular arithmetic operators.
  • Validate all accounts - Always verify account ownership, type, and required permissions.
  • Implement comprehensive error handling - Handle all potential error cases and provide meaningful error messages.
  • Use Rust's type system - Leverage Rust's strong type system to prevent common errors.
  • Minimize trust assumptions - Design your program to minimize trust in external components.
  • Document security considerations - Clearly document expected account structures, validation, and potential security considerations.

Additional Resources