CPI Account List Mismatch Remediation
How to fix CPI calls with unvalidated account lists.
CPI Account List Mismatch Remediation
Overview
Related Detector: CPI Account List Mismatch
CPI account list mismatch vulnerabilities allow attackers to manipulate the accounts passed to cross-program invocations. The fix is to validate account count, properties, and keys before every CPI call.
Recommended Fix
Before (Vulnerable)
pub fn transfer(accounts: &[AccountInfo]) -> ProgramResult {
let ix = spl_token::instruction::transfer(/* ... */);
invoke(&ix, accounts)?; // Account list not validated
Ok(())
}
After (Fixed)
pub fn transfer(accounts: &[AccountInfo]) -> ProgramResult {
// Validate exact account count
if accounts.len() != 5 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Validate critical accounts
if !accounts[2].is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if !accounts[0].is_writable || !accounts[1].is_writable {
return Err(ProgramError::InvalidAccountData);
}
let ix = spl_token::instruction::transfer(/* ... */);
invoke(&ix, accounts)?;
Ok(())
}
Alternative Mitigations
1. Anchor typed accounts
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub source: Account<'info, TokenAccount>,
#[account(mut)]
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
2. Named account extraction with explicit indices
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
let source = accounts.get(0).ok_or(ProgramError::NotEnoughAccountKeys)?;
let dest = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?;
let authority = accounts.get(2).ok_or(ProgramError::NotEnoughAccountKeys)?;
if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); }
// Build account list explicitly
let cpi_accounts = &[source.clone(), dest.clone(), authority.clone()];
invoke(&ix, cpi_accounts)?;
Ok(())
}
Common Mistakes
Mistake 1: Only checking account count, not properties
// INSUFFICIENT: count is right but accounts could be wrong
if accounts.len() == 5 {
invoke(&ix, accounts)?; // No signer/writable checks
}
Mistake 2: Passing remaining_accounts without filtering
// WRONG: remaining_accounts may contain unexpected extra accounts
let all: Vec<_> = ctx.remaining_accounts.iter().collect();
invoke(&ix, &all)?;
Always validate remaining accounts before including them in CPI calls.