CPI Account List Mismatch
Detects CPI calls without proper account list validation, enabling privilege escalation.
CPI Account List Mismatch
Overview
Remediation Guide: How to Fix CPI Account List Mismatch
The CPI account list mismatch detector identifies cross-program invocations where the account list is passed from a variable without validation of its count, order, or properties. An attacker can manipulate the account list to pass extra signer accounts for privilege escalation, reorder accounts to cause confusion, or omit required accounts to trigger unexpected behavior.
Why This Is an Issue
Solana CPI passes accounts by reference. If the account list is dynamically constructed from variables without validation, attackers can inject extra accounts that enable privilege escalation, supply wrong accounts that redirect funds, or reorder accounts to confuse the target program’s account parsing. Real-world exploits including the Wormhole bridge hack involved CPI account list manipulation.
CWE mapping: CWE-269 (Improper Privilege Management).
How to Resolve
Native Solana
const EXPECTED_ACCOUNTS: usize = 5;
pub fn safe_cpi(accounts: &[AccountInfo]) -> ProgramResult {
// Validate account count
if accounts.len() != EXPECTED_ACCOUNTS {
return Err(ProgramError::InvalidAccountData);
}
// Validate account properties
if !accounts[0].is_signer { return Err(ProgramError::MissingRequiredSignature); }
if !accounts[1].is_writable { return Err(ProgramError::InvalidAccountData); }
invoke(&instruction, accounts)?;
Ok(())
}
Anchor
#[derive(Accounts)]
pub struct SafeCpi<'info> {
#[account(signer)]
pub authority: AccountInfo<'info>,
#[account(mut)]
pub vault: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
// Anchor validates count and properties at deserialization
}
Examples
Vulnerable Code
pub fn execute(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let account_list = build_account_list(accounts)?; // Dynamic, unvalidated
let ix = Instruction { program_id: *program.key, accounts: account_list, data: data.to_vec() };
invoke(&ix, accounts)?; // No validation of account list!
Ok(())
}
Fixed Code
pub fn execute(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let account_list = build_account_list(accounts)?;
if account_list.len() != EXPECTED_COUNT { return Err(ProgramError::InvalidAccountData); }
if account_list[0].pubkey != expected_authority { return Err(ProgramError::InvalidAccountData); }
let ix = Instruction { program_id: *program.key, accounts: account_list, data: data.to_vec() };
invoke(&ix, accounts)?;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "cpi-account-list-mismatch",
"severity": "high",
"confidence": 0.55,
"description": "A Cross-Program Invocation passes an account list from a variable without visible validation.",
"location": { "function": "execute", "block": 0, "stmt": 1 }
}
Detection Methodology
- CPI identification: Scans for
InvokeCpistatements in the function. - Account source analysis: Checks whether the accounts expression originates from a variable (not validated inline).
- Validation tracking: Looks for preceding length checks, property validation, or key comparisons on the accounts variable.
- Context adjustment: Confidence is reduced for Anchor programs (CpiContext builds typed account lists) and read-only functions.
Limitations
- The detector flags all CPIs with variable-sourced account lists, which has a moderate false-positive rate.
- Account list validation performed in helper functions is not tracked across call boundaries.
- Anchor programs using
CpiContextare still flagged at reduced confidence, as remaining-accounts patterns can bypass type safety.
Related Detectors
- Cross-Account Relationship — validates relationships between accounts
- Arbitrary CPI — detects unvalidated CPI targets