CPI Signer Simulation
Detects false signer claims in cross-program invocations where accounts were not actually signed in the original transaction.
CPI Signer Simulation
Overview
Remediation Guide: How to Fix CPI Signer Simulation
The CPI signer simulation detector identifies Solana programs that falsely claim signer status for accounts during cross-program invocations. When a program builds a CPI and marks an account as a signer in the AccountMeta, but that account was never validated as a signer in the original transaction, the program effectively forges a signature. This allows unauthorized operations in the target program that require signer authorization.
The detector tracks three patterns:
- Missing signer validation before CPI: An account is passed with
is_signer: truein a CPI without a prioris_signercheck. - Program-as-signer simulation: A program marks itself as signer in a CPI without proper PDA derivation or authority.
- Readonly-to-writable escalation: An account passed as readonly in the original instruction is marked writable in a nested CPI.
Why This Is an Issue
Solana’s runtime enforces signer status at the transaction level, but programs construct CPI instructions manually. If a program sets is_signer: true on an AccountMeta for an account that was not actually signed, the called program will treat it as signed. This creates a privilege escalation where:
- An attacker submits a transaction without signing as the authority account
- The vulnerable program constructs a CPI that claims the authority account signed
- The target program trusts the signer flag and executes the privileged operation
- The attacker performs unauthorized token transfers, state changes, or admin operations
CWE mapping: CWE-269 (Improper Privilege Management), CWE-863 (Incorrect Authorization).
How to Resolve
// Before: Vulnerable -- no signer check before CPI
pub fn transfer_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let authority = &accounts[0];
let token_program = &accounts[3];
// VULNERABLE: authority.is_signer is never checked
let ix = spl_token::instruction::transfer(
token_program.key,
accounts[1].key,
accounts[2].key,
authority.key, // Claimed as signer without verification
&[],
amount,
)?;
invoke(&ix, accounts)?;
Ok(())
}
// After: Validate signer status before using in CPI
pub fn transfer_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let authority = &accounts[0];
let token_program = &accounts[3];
// FIXED: verify the account actually signed the transaction
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let ix = spl_token::instruction::transfer(
token_program.key,
accounts[1].key,
accounts[2].key,
authority.key,
&[],
amount,
)?;
invoke(&ix, accounts)?;
Ok(())
}
Examples
Vulnerable Code
pub fn execute_admin_action(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let admin = &accounts[0];
let target_program = &accounts[1];
// No check that admin.is_signer == true
let ix = Instruction {
program_id: *target_program.key,
accounts: vec![
AccountMeta::new(*admin.key, true), // Claims admin is signer
],
data: data.to_vec(),
};
invoke(&ix, accounts)?; // Admin action executes without real signature
Ok(())
}
Fixed Code
pub fn execute_admin_action(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let admin = &accounts[0];
let target_program = &accounts[1];
// FIXED: verify signer status
if !admin.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// FIXED: validate target program
if target_program.key != &EXPECTED_PROGRAM_ID {
return Err(ProgramError::IncorrectProgramId);
}
let ix = Instruction {
program_id: *target_program.key,
accounts: vec![
AccountMeta::new(*admin.key, true),
],
data: data.to_vec(),
};
invoke(&ix, accounts)?;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "cpi-signer-simulation",
"severity": "critical",
"confidence": 0.85,
"description": "CPI call at block 3, statement 5 marks account v2 as signer, but no CheckSigner validation was found for this account in any dominating block. An attacker can invoke this instruction without signing as the authority and the CPI will still claim signer status.",
"location": { "function": "execute_admin_action", "offset": 5 }
}
Detection Methodology
The detector performs a two-pass analysis over the function’s control flow graph:
- Pass 1 — Collect validation and usage data: Walks all blocks and statements to record (a) accounts validated via
CheckSignerstatements, (b)is_signerfield accesses on account variables, and (c) CPI calls with their account parameters. - Pass 2 — Cross-reference CPI accounts against validated signers: For each CPI call, checks whether every account passed with signer status was previously validated via a
CheckSignerin a dominating block. Accounts that bypass signer validation are flagged. - Escalation detection: When
invoke_signedis used and non-validated accounts are passed, the finding is elevated because PDA signing authority is also delegated. - Context adjustment: Anchor programs receive confidence reduction since the framework enforces signer checks at the account deserialization layer.
Limitations
False positives:
- Programs that validate signer status in a separate function called before the CPI may be flagged if the validation is not inlined.
- Programs using PDA-derived authority (where the PDA itself is the signer) may be flagged if PDA derivation is not recognized.
False negatives:
- Signer validation stored in an external account or registry is not tracked.
- Dynamic signer checks via computed branches may not be recognized.
Related Detectors
- Missing Signer Check — missing signer validation before state-changing operations
- Arbitrary CPI — unvalidated program ID in CPI targets
- CPI Authority Downgrade — privileged accounts passed to unvalidated programs