Skip to main content
Sigvex

CPI Authority Downgrade

Detects privileged accounts being passed to unvalidated programs via CPI, enabling authority hijacking.

CPI Authority Downgrade

Overview

Remediation Guide: How to Fix CPI Authority Downgrade

The CPI authority downgrade detector identifies Solana programs that validate an account’s authority (signer, owner, or key check) and then pass that validated account to an unvalidated external program via CPI. This creates a privilege escalation where the attacker controls the receiving program and gains access to the validated authority.

When invoke_signed is used, the attack is especially severe because the attacker’s program also receives PDA signing authority, enabling it to act as the program on behalf of its PDAs.

The detector tracks three escalating patterns:

  1. Validated signer to unvalidated program: Authority verified but passed to unknown program.
  2. Validated authority with invoke_signed: PDA signing seeds delegated to unknown program.
  3. Multi-authority delegation: Multiple validated accounts forwarded to a single unvalidated CPI target.

Why This Is an Issue

This vulnerability is a privilege escalation through trust delegation. The program correctly validates an account’s authority, which gives a false sense of security. However, passing that validated account to an unvalidated program effectively hands the authority to the attacker:

  • The program verifies that account A is the authorized admin
  • The program then calls an attacker-controlled program, passing account A
  • The attacker’s program now has access to account A with its validated status
  • If invoke_signed is used, the attacker also gains PDA signing ability
  • The attacker can perform any operation that account A is authorized for

This pattern has been found in admin panels, token management, and upgrade authority flows.

CWE mapping: CWE-269 (Improper Privilege Management), CWE-732 (Incorrect Permission Assignment).

How to Resolve

// Before: Vulnerable -- validated authority passed to unvalidated program
pub fn admin_operation(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let admin = &accounts[0];
    let external_program = &accounts[1];

    // Admin is validated
    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // VULNERABLE: external_program is not validated
    let ix = Instruction {
        program_id: *external_program.key,
        accounts: vec![AccountMeta::new(*admin.key, true)],
        data: data.to_vec(),
    };
    invoke(&ix, accounts)?;  // Attacker's program receives admin
    Ok(())
}

// After: Validate the target program before delegating authority
pub fn admin_operation(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let admin = &accounts[0];
    let external_program = &accounts[1];

    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // FIXED: validate the target program
    if external_program.key != &TRUSTED_PROGRAM_ID {
        return Err(ProgramError::IncorrectProgramId);
    }

    let ix = Instruction {
        program_id: *external_program.key,
        accounts: vec![AccountMeta::new(*admin.key, true)],
        data: data.to_vec(),
    };
    invoke(&ix, accounts)?;
    Ok(())
}

Examples

Vulnerable Code

pub fn delegate_mint_authority(accounts: &[AccountInfo]) -> ProgramResult {
    let mint_authority = &accounts[0];
    let mint = &accounts[1];
    let target_program = &accounts[2];

    // Authority is validated
    if !mint_authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    let mint_data = mint.try_borrow_data()?;
    let mint_state = spl_token::state::Mint::unpack(&mint_data)?;
    if mint_state.mint_authority.unwrap() != *mint_authority.key {
        return Err(ProgramError::InvalidAccountData);
    }

    // CRITICAL: validated mint authority sent to unvalidated program
    let ix = Instruction {
        program_id: *target_program.key,  // Attacker-controlled
        accounts: vec![
            AccountMeta::new(*mint_authority.key, true),
            AccountMeta::new(*mint.key, false),
        ],
        data: vec![],
    };
    invoke(&ix, accounts)?;
    Ok(())
}

Fixed Code

const TRUSTED_DELEGATION_PROGRAM: Pubkey = pubkey!("TrUsTeD...");

pub fn delegate_mint_authority(accounts: &[AccountInfo]) -> ProgramResult {
    let mint_authority = &accounts[0];
    let mint = &accounts[1];
    let target_program = &accounts[2];

    if !mint_authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    let mint_data = mint.try_borrow_data()?;
    let mint_state = spl_token::state::Mint::unpack(&mint_data)?;
    if mint_state.mint_authority.unwrap() != *mint_authority.key {
        return Err(ProgramError::InvalidAccountData);
    }

    // FIXED: validate the target program
    if target_program.key != &TRUSTED_DELEGATION_PROGRAM {
        return Err(ProgramError::IncorrectProgramId);
    }

    let ix = Instruction {
        program_id: *target_program.key,
        accounts: vec![
            AccountMeta::new(*mint_authority.key, true),
            AccountMeta::new(*mint.key, false),
        ],
        data: vec![],
    };
    invoke(&ix, accounts)?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "cpi-authority-downgrade",
  "severity": "critical",
  "confidence": 0.85,
  "description": "Validated signer account v0 is passed to an unvalidated program via invoke_signed at block 4. The target program has no CheckKey validation, allowing an attacker to receive both the validated authority and PDA signing capability.",
  "location": { "function": "delegate_mint_authority", "offset": 7 }
}

Detection Methodology

The detector uses CFG-aware dataflow analysis:

  1. Dataflow analysis: Runs DataflowAnalyzer to propagate validation state (signer checks, owner checks, key checks) across basic blocks, tracking variable aliases.
  2. Validated account identification: Collects all accounts that have been validated via CheckSigner, CheckOwner, or CheckKey in dominating blocks.
  3. CPI account extraction: For each InvokeCpi statement, extracts the set of account variables passed to the CPI.
  4. Program validation check: Determines whether the CPI target program has been validated via CheckKey. If not, and if validated accounts are included in the CPI accounts, a finding is emitted.
  5. Severity escalation: When invoke_signed (seeds present) is used with unvalidated programs, severity is escalated because PDA signing authority is also delegated to the attacker.

Limitations

False positives:

  • Programs that validate the CPI target in a separate function or instruction may be flagged.
  • Well-known program IDs (System, SPL Token) used via constants that are not recognized as trusted.

False negatives:

  • Authority validation through governance PDAs or multi-sig patterns may not be recognized.
  • Indirect CPI through wrapper functions that obscure the authority delegation.

References