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:
- Arbitrary
invoke: CPI to an unvalidated program ID (Critical, confidence 0.82) - Arbitrary
invoke_signed: CPI with seeds to an unvalidated program ID — grants PDA signing authority to the attacker (Critical, confidence 0.88) - Privilege escalation via
invoke_signedwith 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_signedis 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:
- Early exit: Skips functions with no
InvokeCpistatements. - Dataflow analysis: Runs
DataflowAnalyzer::analyze()to propagateCheckKeyvalidations across blocks. - Constant tracking:
ConstantTrackerperforms constant propagation — if the program ID is a hardcodedConst(_)or assigned from a constant, it is treated as trusted (true negative). - Seed detection: Checks
InvokeCpi.seedsfield — ifSome, the call isinvoke_signed. - Signer account detection:
check_accounts_have_signers()traverses the accounts expression to find validated signer accounts. - 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).
- 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.
Related Detectors
- Missing Signer Check — missing signer validation before operations
- Missing Owner Check — detects missing account ownership validation in CPI contexts