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.
Recommended Fix
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.