Skip to main content
Sigvex

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.

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.

References