Critical Program ID Validation Remediation
How to fix missing validation of critical system program IDs before CPI.
Critical Program ID Validation Remediation
Overview
Related Detector: Critical Program ID Validation
This vulnerability occurs when a program makes CPI calls to critical system programs (System Program, SPL Token, Token-2022, Metaplex) without verifying the target program ID against known constants. The fix is to validate the program ID before every CPI to a system program.
Recommended Fix
Before (Vulnerable)
pub fn create_vault(accounts: &[AccountInfo]) -> ProgramResult {
let system_program = &accounts[3]; // Not validated
let ix = system_instruction::create_account(/* ... */);
invoke(&ix, accounts)?; // Could invoke attacker's program
Ok(())
}
After (Fixed)
use solana_program::system_program;
pub fn create_vault(accounts: &[AccountInfo]) -> ProgramResult {
let system_program = &accounts[3];
// Validate against known constant
if system_program.key != &system_program::id() {
return Err(ProgramError::IncorrectProgramId);
}
let ix = system_instruction::create_account(/* ... */);
invoke(&ix, accounts)?;
Ok(())
}
Alternative Mitigations
1. Anchor Program<'info, T> type
Anchor validates the program ID automatically at account deserialization:
#[derive(Accounts)]
pub struct CreateVault<'info> {
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
2. Constant declarations for all critical programs
Define constants at module level for clarity and reuse:
use solana_program::pubkey::Pubkey;
const SYSTEM_PROGRAM: Pubkey = solana_program::system_program::id();
const TOKEN_PROGRAM: Pubkey = spl_token::id();
const TOKEN_2022_PROGRAM: Pubkey = spl_token_2022::id();
fn validate_program(program: &AccountInfo, expected: &Pubkey) -> ProgramResult {
if program.key != expected {
return Err(ProgramError::IncorrectProgramId);
}
Ok(())
}
3. Whitelist for programs that accept multiple token standards
const VALID_TOKEN_PROGRAMS: &[Pubkey] = &[
spl_token::id(),
spl_token_2022::id(),
];
pub fn flexible_transfer(accounts: &[AccountInfo]) -> ProgramResult {
let token_program = &accounts[3];
if !VALID_TOKEN_PROGRAMS.contains(token_program.key) {
return Err(ProgramError::IncorrectProgramId);
}
invoke(/* ... */, accounts)?;
Ok(())
}
Common Mistakes
Mistake 1: Checking only the executable flag
// WRONG: any executable program passes this
if !system_program.executable {
return Err(ProgramError::InvalidAccountData);
}
// Still vulnerable -- does not check which program it is
Mistake 2: Comparing against a variable instead of a constant
// WRONG: expected_id could itself be attacker-controlled
let expected_id = &accounts[5].key;
if system_program.key != expected_id { /* ... */ }
Always compare against compile-time constants or program-defined Pubkey values.
Mistake 3: Validating after the CPI
invoke(&ix, accounts)?; // Already executed
if program.key != &EXPECTED_ID { /* too late */ }