Skip to main content
Sigvex

CPI Signer Propagation Remediation

How to fix unsafe signer propagation in nested CPI calls.

CPI Signer Propagation Remediation

Overview

Related Detector: CPI Signer Propagation

CPI signer propagation vulnerabilities occur when PDA-signed CPI calls pass signing authority to called programs, and subsequent operations proceed without re-validating that the signer context is still valid. The fix is to re-check signer status after every CPI that grants PDA authority.

Before (Vulnerable)

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    invoke_signed(&ix_a, accounts, &[&seeds])?;
    // No signer re-check
    invoke(&ix_b, accounts)?; // Signer context may be stale
    Ok(())
}

After (Fixed)

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

    invoke_signed(&ix_a, accounts, &[&seeds])?;

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

    invoke(&ix_b, accounts)?;
    Ok(())
}

Alternative Mitigations

1. Use separate PDA authorities

Isolate signing authority by using different PDAs for different operations:

let (vault_pda, vault_bump) = Pubkey::find_program_address(&[b"vault"], program_id);
let (escrow_pda, escrow_bump) = Pubkey::find_program_address(&[b"escrow"], program_id);

// Each CPI uses its own PDA -- no authority leakage between operations
invoke_signed(&vault_ix, accounts, &[&[b"vault", &[vault_bump]]])?;
invoke_signed(&escrow_ix, accounts, &[&[b"escrow", &[escrow_bump]]])?;

2. Checks-effects-interactions pattern

Perform all state reads and validations before any CPI, then make CPI calls, then write state:

// CHECKS: validate everything first
require!(authority.is_signer, MissingSigner);
let balance = get_balance(vault)?;
require!(balance >= amount, InsufficientFunds);

// INTERACTIONS: make CPI calls
invoke_signed(&transfer_ix, accounts, &[&seeds])?;

// EFFECTS: update local state last
update_balance(vault, balance - amount)?;

3. Minimize CPI chain depth

Refactor deep CPI chains into flatter architectures:

// Instead of: A -> B -> C -> D (deep chain)
// Refactor to: A -> B, A -> C, A -> D (flat chain with re-validation)

Common Mistakes

Mistake 1: Assuming signer status persists after CPI

// WRONG: signer check before CPI does not guarantee status after CPI
if authority.is_signer {
    invoke_signed(&ix, accounts, &[&seeds])?;
    // Authority may no longer be valid here
    modify_state(accounts)?; // Dangerous
}

Mistake 2: Only checking the first CPI in a chain

// WRONG: validating only before the first CPI
require!(authority.is_signer);
invoke_signed(&ix_a, accounts, &[&seeds])?;
invoke(&ix_b, accounts)?; // No re-check before second CPI
invoke(&ix_c, accounts)?; // No re-check before third CPI

Re-validate before every sensitive operation after a CPI.

References