Remediating CPI Program ID Validation
How to ensure Cross-Program Invocations target the intended program by validating program account keys before invocation.
Remediating CPI Program ID Validation
Overview
Related Detector: CPI Program ID Validation
Missing program ID validation allows an attacker to substitute a malicious program in any position that your instruction uses for a CPI target. The fix is straightforward: compare the program account’s key against the expected program ID before invoking, or use Anchor’s Program<'info, T> type which performs this check automatically at account deserialization.
Recommended Fix
Option 1: Use Anchor’s Program Type (Preferred)
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub source: Account<'info, TokenAccount>,
#[account(mut)]
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
/// Program<'info, Token> automatically validates:
/// - account.key() == Token::id() (spl_token::id())
/// - account is executable
pub token_program: Program<'info, Token>,
}
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.source.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
);
token::transfer(cpi_ctx, amount)?;
Ok(())
}
Option 2: Explicit Key Check (Native Programs)
use solana_program::{
account_info::AccountInfo,
program_error::ProgramError,
program::invoke,
};
pub fn transfer_tokens(
accounts: &[AccountInfo],
amount: u64,
) -> Result<(), ProgramError> {
let source = &accounts[0];
let dest = &accounts[1];
let authority = &accounts[2];
let token_program = &accounts[3];
// Explicit check before any CPI
if *token_program.key != spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
// Also verify executability as a secondary check
if !token_program.executable {
return Err(ProgramError::InvalidAccountData);
}
let transfer_ix = spl_token::instruction::transfer(
token_program.key,
source.key,
dest.key,
authority.key,
&[],
amount,
)?;
invoke(
&transfer_ix,
&[source.clone(), dest.clone(), authority.clone(), token_program.clone()],
)?;
Ok(())
}
Alternative Mitigations
Hardcode the Program ID in the Instruction
For well-known programs (SPL Token, System Program, Associated Token Account), remove the program account from the instruction accounts entirely and hardcode the program ID:
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
// No need to accept token_program as an account — it's hardcoded in the CPI
let cpi_accounts = Transfer {
from: ctx.accounts.source.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
// CpiContext automatically uses the token program ID from the Token type
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
token::transfer(cpi_ctx, amount)?;
Ok(())
}
#[derive(Accounts)]
pub struct TransferTokens<'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>, // Typed and validated
}
Common Mistakes
Mistake: Checking Executable Flag Only
// INSUFFICIENT: any deployed program is executable
if !token_program.executable {
return Err(ProgramError::InvalidAccountData);
}
// Missing: if *token_program.key != spl_token::id() { ... }
The executable flag only confirms the account contains deployed program code. It does not confirm which program it is.
Mistake: Validating Owner Instead of Key
// WRONG: program account owner is the BPF loader, not the program itself
if token_program.owner != &spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
// Correct check: if *token_program.key != spl_token::id() { ... }
Program accounts are owned by the BPF Loader programs, not by themselves. The program’s identity is its key (address), not its owner.