Skip to main content
Sigvex

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, the NFT metadata program) without verifying the target program ID against known constants. The fix is to validate the program ID before every CPI to a system program.

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 */ }

References