Skip to main content
Sigvex

Account Executable Flag

Detects missing executable flag validation before using accounts as program IDs in cross-program invocations.

Account Executable Flag

Overview

Remediation Guide: How to Fix Missing Executable Flag Validation

The account executable flag detector identifies Solana programs that use accounts as program IDs in cross-program invocations (CPI) without verifying that the account is marked as executable. On Solana, accounts are either program accounts (executable = true) or data accounts (executable = false). When making a CPI, the target must be a program account. If the code does not verify the executable flag or validate the program ID against a known constant, an attacker can substitute a data account, causing unexpected behavior.

Sigvex tracks CPI calls (InvokeCpi statements and Cpi expressions), program ID validations (CheckKey with constant values), and hardcoded program constants. Unvalidated variable program IDs used in CPI are flagged. CWE mapping: CWE-20 (Improper Input Validation).

Why This Is an Issue

While the Solana runtime enforces that CPI targets must be executable (the transaction will fail if a data account is passed), explicit validation provides defense-in-depth:

  • Clear error messages: Without explicit checks, the runtime produces generic errors that are difficult to debug.
  • Prevent confusion attacks: An attacker passing a data account can cause the program to follow error handling paths that may have unintended side effects.
  • Composability safety: In complex multi-CPI chains, ensuring each program ID is validated prevents cascading failures.
  • Future-proofing: If runtime behavior changes or the program is deployed to a fork with different validation rules, explicit checks maintain security.

How to Resolve

Native Rust

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};

const EXPECTED_PROGRAM: Pubkey = /* known program ID */;

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

    // Validate program identity (implicitly ensures executability)
    if program.key != &EXPECTED_PROGRAM {
        return Err(ProgramError::IncorrectProgramId);
    }

    // Optionally check executable flag directly
    if !program.executable {
        return Err(ProgramError::InvalidAccountData);
    }

    solana_program::program::invoke(
        &build_instruction(program.key),
        &[accounts[1].clone()],
    )?;
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct InvokeExternal<'info> {
    // Anchor's Program<T> type validates program identity and executability
    pub target_program: Program<'info, MyProgram>,
}

Examples

Vulnerable Code

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

pub fn delegate_operation(accounts: &[AccountInfo]) -> ProgramResult {
    let program_account = &accounts[0]; // User-supplied -- no validation!
    let data_account = &accounts[1];

    // CPI with unvalidated program -- attacker passes a data account
    solana_program::program::invoke(
        &build_instruction(program_account.key),
        &[data_account.clone()],
    )?;
    Ok(())
}

Fixed Code

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};

const APPROVED_PROGRAM: Pubkey = /* known program address */;

pub fn delegate_operation(accounts: &[AccountInfo]) -> ProgramResult {
    let program_account = &accounts[0];
    let data_account = &accounts[1];

    // Validate program identity
    if program_account.key != &APPROVED_PROGRAM {
        return Err(ProgramError::IncorrectProgramId);
    }

    solana_program::program::invoke(
        &build_instruction(program_account.key),
        &[data_account.clone()],
    )?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "account-executable-flag",
  "severity": "high",
  "confidence": 0.85,
  "description": "Account v1 is used as a program ID in a cross-program invocation without validating that it is an executable program account.",
  "location": { "function": "delegate_operation", "offset": 1 }
}

Detection Methodology

The detector performs a two-pass analysis:

  1. Collection pass: Identifies all CPI calls (both InvokeCpi statements and Cpi expressions nested in other expressions), and all program ID validations (CheckKey against constant values).
  2. Validation pass: For each CPI call, checks whether the program ID is either a hardcoded constant (compile-time safe) or has been validated via CheckKey against a constant. Unvalidated program IDs generate findings.

Context modifiers:

  • Anchor programs: confidence reduced by 50% (Program<'info, T> validates program identity)
  • Admin/initialization functions: confidence reduced by 40%
  • Read-only/view functions: confidence reduced by 60%

Limitations

False positives:

  • Programs that validate program IDs through indirect means (e.g., reading a config account that stores the expected program ID) may be flagged because the validation is not a direct CheckKey with a constant.
  • Programs where the CPI target is always a well-known program passed by the runtime (e.g., system program) may be flagged if the variable is not checked.

False negatives:

  • Validation against a non-constant variable (e.g., another account’s key) is not recognized as safe, since the variable itself could be attacker-controlled. This is by design.
  • CPI calls hidden behind complex expression trees beyond the detector’s recursive depth may be missed.

References