CPI Authority Downgrade Remediation
How to prevent privileged accounts from being delegated to unvalidated programs via cross-program invocations.
CPI Authority Downgrade Remediation
Overview
Related Detector: CPI Authority Downgrade
CPI authority downgrade occurs when a program validates an account’s authority and then passes that account to an unvalidated external program. The fix is to always validate the target program’s identity before any CPI that includes privileged accounts.
Recommended Fix
Before (Vulnerable)
pub fn process_with_authority(accounts: &[AccountInfo]) -> ProgramResult {
let authority = &accounts[0];
let target = &accounts[1];
// Authority validated
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Target program NOT validated -- attacker controls it
let ix = Instruction {
program_id: *target.key,
accounts: vec![AccountMeta::new(*authority.key, true)],
data: vec![],
};
invoke(&ix, accounts)?;
Ok(())
}
After (Fixed)
pub fn process_with_authority(accounts: &[AccountInfo]) -> ProgramResult {
let authority = &accounts[0];
let target = &accounts[1];
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// FIXED: validate target program before delegating authority
if target.key != &EXPECTED_PROGRAM_ID {
return Err(ProgramError::IncorrectProgramId);
}
let ix = Instruction {
program_id: *target.key,
accounts: vec![AccountMeta::new(*authority.key, true)],
data: vec![],
};
invoke(&ix, accounts)?;
Ok(())
}
Alternative Mitigations
1. Anchor Program<'info, T> type
Anchor validates program identity at deserialization, preventing authority downgrade:
#[derive(Accounts)]
pub struct DelegateAction<'info> {
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>, // Validated automatically
}
2. Minimize authority exposure
Instead of passing the authority to external programs, use PDA-based authority where the calling program maintains control:
// Instead of delegating user authority to external program,
// use a PDA that the calling program controls
let seeds = &[b"authority", user.key.as_ref(), &[bump]];
invoke_signed(&ix, accounts, &[seeds])?;
3. Whitelist of trusted programs
For cases where multiple programs may be valid targets:
const TRUSTED_PROGRAMS: &[Pubkey] = &[
TOKEN_PROGRAM_ID,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
];
if !TRUSTED_PROGRAMS.contains(target.key) {
return Err(ProgramError::IncorrectProgramId);
}
Common Mistakes
Mistake 1: Validating the program after the CPI
invoke(&ix, accounts)?; // Authority already exposed
if target.key != &EXPECTED_ID {
return Err(ProgramError::IncorrectProgramId); // Too late
}
Mistake 2: Only checking that the program is executable
if !target.executable {
return Err(ProgramError::InvalidAccountData);
}
// Still vulnerable -- any executable program passes
invoke(&ix, accounts)?;
Checking executable verifies the account is a program but not which program. Always check the specific key.
Mistake 3: Splitting validation and CPI across different code paths
if condition_a {
if target.key != &EXPECTED_ID {
return Err(ProgramError::IncorrectProgramId);
}
}
// Attacker takes !condition_a path -- no validation
invoke(&ix_with_authority, accounts)?;
The program validation must dominate the CPI on all execution paths.