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.
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(()) }
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(()) }
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(()) }
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) }
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(()) }
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(()) }
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(()) }
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(()) }
In addition to identifying specific vulnerabilities, Believe Security provides recommendations for following security best practices:
checked_add
, checked_sub
) instead of regular arithmetic operators.