Skip to main content
Sigvex

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.

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.

References