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.