Skip to main content
Sigvex

CPI Signer Propagation

Detects unsafe signer propagation in nested CPI calls without re-validation.

CPI Signer Propagation

Overview

Remediation Guide: How to Fix CPI Signer Propagation

The CPI signer propagation detector identifies cases where signing authority is automatically inherited through nested CPI calls without explicit re-validation at each hop. In Solana, when a program invokes another program using invoke_signed with PDA seeds, the called program inherits signing authority. In complex CPI chains, this automatic propagation can lead to privilege escalation if signers are not re-validated between invocations.

The detector flags three escalating patterns:

  1. Sequential CPIs without signer re-validation between a PDA-signed CPI and a subsequent CPI (High).
  2. State modification after PDA-signed CPI without re-checking signer status (High).
  3. Deep PDA-signed CPI chains with three or more signed CPIs, compounding authorization risk (Critical).

Why This Is an Issue

When a program makes a PDA-signed CPI, the called program can act with the PDA’s signing authority. If the calling program then makes a second CPI or modifies state without re-validating the signer, the intermediate program may have changed the authorization context. An attacker can exploit this gap to perform unauthorized operations in downstream programs or corrupt local state based on stale signer assumptions.

CWE mapping: CWE-863 (Incorrect Authorization), CWE-367 (TOCTOU Race Condition), CWE-250 (Execution with Unnecessary Privileges).

How to Resolve

Native Solana

pub fn multi_step(accounts: &[AccountInfo]) -> ProgramResult {
    let authority = &accounts[0];

    // First CPI with PDA signing
    invoke_signed(&first_ix, accounts, &[&seeds])?;

    // Re-validate signer before next operation
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Second CPI is now safe
    invoke(&second_ix, accounts)?;
    Ok(())
}

Anchor

pub fn multi_step(ctx: Context<MultiStep>) -> Result<()> {
    // First CPI
    let cpi_ctx = CpiContext::new_with_signer(/* ... */);
    token::transfer(cpi_ctx, amount)?;

    // Re-validate signer after CPI
    require!(ctx.accounts.authority.is_signer, ErrorCode::MissingSigner);

    // Safe to proceed with second operation
    // ...
    Ok(())
}

Examples

Vulnerable Code

pub fn compound_transfer(accounts: &[AccountInfo]) -> ProgramResult {
    // PDA-signed CPI to token program
    invoke_signed(&transfer_ix, accounts, &[&pda_seeds])?;

    // State modification without re-checking signer -- VULNERABLE
    let vault = &accounts[2];
    let mut data = vault.data.borrow_mut();
    data[0..8].copy_from_slice(&new_balance.to_le_bytes());
    Ok(())
}

Fixed Code

pub fn compound_transfer(accounts: &[AccountInfo]) -> ProgramResult {
    invoke_signed(&transfer_ix, accounts, &[&pda_seeds])?;

    // Re-validate authority after CPI
    let authority = &accounts[0];
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let vault = &accounts[2];
    let mut data = vault.data.borrow_mut();
    data[0..8].copy_from_slice(&new_balance.to_le_bytes());
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "cpi-signer-propagation",
  "severity": "high",
  "confidence": 0.80,
  "description": "PDA-signed CPI at block 0 stmt 0 is followed by another CPI without signer re-validation.",
  "location": { "function": "compound_transfer", "block": 0, "stmt": 2 }
}

Detection Methodology

  1. CPI collection: Scans for all InvokeCpi statements, noting which have PDA seeds.
  2. Signer check tracking: Records CheckSigner statements and their positions.
  3. Gap analysis: For sequential CPI pairs where the first has seeds, checks if a signer validation exists between them.
  4. State mutation tracking: Detects StoreAccountData, TransferLamports, and Store operations after PDA-signed CPIs without intervening signer checks.
  5. Depth analysis: Flags functions with three or more PDA-signed CPIs as deep chains with compounded risk.

Limitations

  • Signer validation performed inside called functions is not visible at the caller level.
  • The detector cannot track signer status changes across multiple blocks with complex control flow.
  • Functions that use separate PDA authorities for different operations may be flagged if the structure is not visible in the HIR.

References