Skip to main content
Sigvex

Arbitrary Cross-Program Invocation

Detects cross-program invocations (CPI) where the target program ID is user-controlled and not validated, allowing attackers to redirect calls to malicious programs.

Arbitrary Cross-Program Invocation

Overview

Remediation Guide: How to Fix Arbitrary Cross-Program Invocation

The arbitrary CPI detector (v3.0) identifies Solana programs that invoke other programs via invoke() or invoke_signed() without first validating the target program’s ID. In Solana, cross-program invocation is the mechanism by which programs call other programs. If an attacker can supply the program ID to be invoked, they can redirect the invocation to a malicious program that receives the calling program’s account context — including any PDA signing authority granted via invoke_signed.

This detector distinguishes three escalating severity patterns:

  1. Arbitrary invoke: CPI to an unvalidated program ID (Critical, confidence 0.82)
  2. Arbitrary invoke_signed: CPI with seeds to an unvalidated program ID — grants PDA signing authority to the attacker (Critical, confidence 0.88)
  3. Privilege escalation via invoke_signed with signer accounts: The most dangerous pattern — the attacker’s program receives both PDA authority and validated signer accounts (Critical)

Why This Is an Issue

An arbitrary CPI is effectively an “execute arbitrary code with my privileges” vulnerability. The target program receives:

  • All accounts passed by the invoking program (including mutable accounts with funds)
  • If invoke_signed is used: the ability to sign on behalf of the invoking program’s PDAs

The Solend governance attack (November 2022) and multiple smaller Solana protocol exploits have exploited unvalidated CPI targets to drain token accounts, override program state, or impersonate program-owned accounts.

CWE mapping: CWE-20 (Improper Input Validation), CWE-269 (Improper Privilege Management) for signed variants.

How to Resolve

// Before: Vulnerable — program ID not validated
pub fn swap(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let program = &accounts[2];  // Could be any program!
    let instruction = build_swap_instruction(amount);
    invoke(&instruction, accounts)?;  // CRITICAL: arbitrary program executes
    Ok(())
}

// After: Validate program ID before CPI
pub fn swap(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let program = &accounts[2];

    // Validate it's the expected DEX program
    if program.key != &EXPECTED_DEX_PROGRAM_ID {
        return Err(ProgramError::IncorrectProgramId);
    }

    let instruction = build_swap_instruction(amount);
    invoke(&instruction, accounts)?;
    Ok(())
}

For Anchor programs:

// Anchor: Program<'info, T> validates program ID at deserialization
#[derive(Accounts)]
pub struct Swap<'info> {
    pub token_program: Program<'info, Token>,       // Validates it's the SPL Token program
    pub associated_token_program: Program<'info, AssociatedToken>, // Validates ATA program
}

// The CPI within this context is always to the validated program
pub fn swap(ctx: Context<Swap>, amount: u64) -> Result<()> {
    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        // ...
    );
    token::transfer(cpi_ctx, amount)?;
    Ok(())
}

Examples

Vulnerable Code

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    instruction::Instruction,
    program::invoke,
};

// Vulnerable: accepts any program from accounts slice
pub fn execute_trade(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let trading_program = &accounts[4]; // Caller-controlled — not validated

    let instruction = Instruction {
        program_id: *trading_program.key,
        accounts: accounts.iter().map(|a| {
            AccountMeta { pubkey: *a.key, is_signer: a.is_signer, is_writable: a.is_writable }
        }).collect(),
        data: data.to_vec(),
    };

    // CRITICAL: malicious program now executes with full account access
    invoke(&instruction, accounts)?;
    Ok(())
}

Fixed Code

use solana_program::{pubkey, pubkey::Pubkey};

const TRUSTED_TRADING_PROGRAM: Pubkey = pubkey!("TrAdEXxxx...");

pub fn execute_trade(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let trading_program = &accounts[4];

    // FIXED: validate program identity before CPI
    if trading_program.key != &TRUSTED_TRADING_PROGRAM {
        msg!("Invalid trading program: {}", trading_program.key);
        return Err(ProgramError::IncorrectProgramId);
    }

    let instruction = Instruction {
        program_id: *trading_program.key,
        accounts: /* ... */,
        data: data.to_vec(),
    };
    invoke(&instruction, accounts)?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "arbitrary-cpi",
  "severity": "critical",
  "confidence": 0.88,
  "description": "CPI with seeds (invoke_signed) is made to unvalidated program v2 with accounts that have signer authority. This grants signing authority to an attacker-controlled program, allowing them to impersonate the PDA and perform privileged operations like transferring tokens, upgrading programs, or modifying admin state. This is a CRITICAL privilege escalation vulnerability.",
  "location": { "function": "execute_trade", "offset": 8 }
}

Detection Methodology

The detector implements CFG-based dataflow analysis with constant propagation:

  1. Early exit: Skips functions with no InvokeCpi statements.
  2. Dataflow analysis: Runs DataflowAnalyzer::analyze() to propagate CheckKey validations across blocks.
  3. Constant tracking: ConstantTracker performs constant propagation — if the program ID is a hardcoded Const(_) or assigned from a constant, it is treated as trusted (true negative).
  4. Seed detection: Checks InvokeCpi.seeds field — if Some, the call is invoke_signed.
  5. Signer account detection: check_accounts_have_signers() traverses the accounts expression to find validated signer accounts.
  6. Escalating severity: Regular invoke → “Arbitrary CPI” (confidence 0.82); invoke_signed without signers → “Arbitrary Signed CPI” (confidence 0.88); invoke_signed with signers → “Privilege Escalation” (confidence 0.88).
  7. Anchor context: Programs with discriminator validation reduce confidence by 0.15; Anchor-like programs without full validation reduce by 0.30.

Known safe programs (System, SPL Token, SPL Token-2022, ATA, Sysvar, Vote, Stake, Config) are documented in TRUSTED_PROGRAM_IDS and used in recommendations.

Limitations

False positives:

  • Programs that validate the program ID inside a called function (not inlined) may be flagged.
  • Programs that use an on-chain registry of trusted program IDs (validated at the registry level) may be flagged if the validation is not visible at the instruction level.

False negatives:

  • Program IDs loaded from a storage slot that was previously validated are not tracked across function calls.
  • Whitelist-based validation (checking against a list of allowed programs) may not be fully recognized.

References