Skip to main content
Sigvex

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:

  1. Missing signer validation before CPI: An account is passed with is_signer: true in a CPI without a prior is_signer check.
  2. Program-as-signer simulation: A program marks itself as signer in a CPI without proper PDA derivation or authority.
  3. 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:

  1. Pass 1 – Collect validation and usage data: Walks all blocks and statements to record (a) accounts validated via CheckSigner statements, (b) is_signer field accesses on account variables, and (c) CPI calls with their account parameters.
  2. 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 CheckSigner in a dominating block. Accounts that bypass signer validation are flagged.
  3. Escalation detection: When invoke_signed is used and non-validated accounts are passed, the finding is elevated because PDA signing authority is also delegated.
  4. 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.

References