CPI Signer Simulation Remediation
How to prevent false signer claims in cross-program invocations by validating signer status before CPI calls.
CPI Signer Simulation Remediation
Overview
Related Detector: CPI Signer Simulation
CPI signer simulation vulnerabilities occur when a program marks an account as a signer in a cross-program invocation without first verifying that the account actually signed the transaction. The fix is to check account.is_signer before every CPI that uses the account with signer status.
Recommended Fix
Before (Vulnerable)
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let owner = &accounts[0];
let vault = &accounts[1];
let token_program = &accounts[3];
// No signer check on owner
let ix = spl_token::instruction::transfer(
token_program.key,
vault.key,
accounts[2].key,
owner.key, // Passed as signer -- not verified
&[],
amount,
)?;
invoke(&ix, accounts)?;
Ok(())
}
After (Fixed)
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let owner = &accounts[0];
let vault = &accounts[1];
let token_program = &accounts[3];
// FIXED: verify signer before CPI
if !owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// FIXED: validate token program
if token_program.key != &spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
let ix = spl_token::instruction::transfer(
token_program.key,
vault.key,
accounts[2].key,
owner.key,
&[],
amount,
)?;
invoke(&ix, accounts)?;
Ok(())
}
Alternative Mitigations
1. Anchor Signer<'info> type
Anchor enforces signer validation automatically at deserialization:
#[derive(Accounts)]
pub struct Withdraw<'info> {
pub owner: Signer<'info>, // Automatically checks is_signer
#[account(mut, has_one = owner)]
pub vault: Account<'info, TokenAccount>,
#[account(mut)]
pub destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
2. PDA-based authority
Use a PDA as the authority instead of requiring a user signature in the CPI:
let seeds = &[b"vault_authority", vault.key.as_ref(), &[bump]];
let signer_seeds = &[&seeds[..]];
invoke_signed(&ix, accounts, signer_seeds)?;
With PDA authority, the program itself controls signing and no user signer claim is needed.
Common Mistakes
Mistake 1: Checking signer status after the CPI
invoke(&ix, accounts)?; // Too late -- CPI already executed
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
The signer check must occur before the CPI, not after.
Mistake 2: Checking a different account than the one used in the CPI
// Checks accounts[0] but CPI uses accounts[1] as signer
if !accounts[0].is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let ix = build_instruction(accounts[1].key, /* is_signer: true */);
invoke(&ix, accounts)?;
Ensure the signer check targets the exact account used as signer in the CPI.
Mistake 3: Only checking signer on one CPI when multiple exist
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(&first_ix, accounts)?; // OK
// Second CPI uses a different authority -- no check
invoke(&second_ix, other_accounts)?; // VULNERABLE
Every CPI that claims signer status must have its own corresponding signer check.