Skip to main content
Sigvex

Account Verification Chain

Detects incomplete account verification chains where accounts undergo partial validation, allowing attackers to bypass security checks.

Account Verification Chain

Overview

Remediation Guide: How to Fix Incomplete Account Verification Chains

The account verification chain detector identifies Solana programs where accounts are only partially validated – for example, checking the signer status but not the owner, or checking the key but not the owner. A complete verification chain for critical accounts should include signer validation, owner validation, and key validation. Partial verification allows attackers to provide accounts that satisfy some requirements but not others.

Sigvex tracks three categories of validation checks (CheckSigner, CheckOwner, CheckKey) for each account variable, and identifies accounts used in sensitive operations (state modifications, lamport transfers). Accounts with only one validation check out of three possible checks are flagged, with elevated severity when the account is used in sensitive operations. CWE mapping: CWE-863 (Incorrect Authorization).

Why This Is an Issue

Each validation check serves a distinct purpose:

  • CheckSigner: Verifies WHO authorized the transaction (the private key holder signed it).
  • CheckOwner: Verifies WHAT program owns the account (prevents cross-program account confusion).
  • CheckKey: Verifies WHICH specific account is being used (prevents account substitution).

Common vulnerability patterns from incomplete chains:

  • Key without owner: An attacker creates a fake account with the correct key but owned by a malicious program, injecting crafted data.
  • Signer without owner: A signer can control any account they own, but without an owner check, the account could be from a different program with incompatible state.
  • Owner without signer/key: Any account owned by the expected program can be passed, but without authorization, an attacker uses someone else’s account.

How to Resolve

Native Rust

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

const EXPECTED_ACCOUNT: Pubkey = /* specific account */;
const EXPECTED_OWNER: Pubkey = /* owning program */;

pub fn sensitive_operation(accounts: &[AccountInfo]) -> ProgramResult {
    let authority = &accounts[0];
    let target = &accounts[1];

    // Complete verification chain for authority
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Complete verification chain for target
    if target.key != &EXPECTED_ACCOUNT {
        return Err(ProgramError::InvalidArgument);
    }
    if target.owner != &EXPECTED_OWNER {
        return Err(ProgramError::IllegalOwner);
    }

    // Safe to modify
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct SensitiveOperation<'info> {
    // Signer type + constraint = complete chain for authority
    #[account(constraint = authority.key() == ADMIN @ ErrorCode::Unauthorized)]
    pub authority: Signer<'info>,

    // Owner + key validation via Account type + constraint
    #[account(
        mut,
        constraint = target.key() == EXPECTED_ACCOUNT @ ErrorCode::WrongAccount
    )]
    pub target: Account<'info, MyState>,
}

Examples

Vulnerable Code

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

pub fn update_config(accounts: &[AccountInfo], new_value: u64) -> ProgramResult {
    let admin = &accounts[0];
    let config = &accounts[1];

    // Only checks signer -- no owner or key check!
    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Writes to config without verifying ownership
    let mut data = config.data.borrow_mut();
    data[0..8].copy_from_slice(&new_value.to_le_bytes());
    Ok(())
}

Fixed Code

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

const ADMIN_KEY: Pubkey = /* admin address */;

pub fn update_config(accounts: &[AccountInfo], new_value: u64) -> ProgramResult {
    let admin = &accounts[0];
    let config = &accounts[1];

    // Signer check
    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    // Key check
    if admin.key != &ADMIN_KEY {
        return Err(ProgramError::InvalidArgument);
    }
    // Owner check on config
    if config.owner != &MY_PROGRAM_ID {
        return Err(ProgramError::IllegalOwner);
    }

    let mut data = config.data.borrow_mut();
    data[0..8].copy_from_slice(&new_value.to_le_bytes());
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "account-verification-chain",
  "severity": "high",
  "confidence": 0.80,
  "description": "Account variable 1 has incomplete verification. Present checks: CheckSigner. Missing checks: CheckOwner, CheckKey. This account is used in sensitive operations (state modification or transfers).",
  "location": { "function": "update_config", "offset": 0 }
}

Detection Methodology

The detector performs four targeted checks:

  1. Incomplete chain: Accounts with only one of three possible validation checks (signer, owner, key) are flagged. Severity is elevated when the account is used in sensitive operations.
  2. Key without owner: Accounts validated by key but not by owner are flagged as high severity, since a fake account with the correct key but wrong owner can bypass key-based checks.
  3. Signer without owner for sensitive ops: Accounts used in state modifications that are signer-checked but not owner-checked are flagged as high severity.
  4. Owner without authorization: Accounts with only an owner check and no signer or key check are flagged as medium severity.

Context modifiers:

  • Anchor programs: confidence reduced by 60% (Anchor’s #[account] constraints enforce full validation)
  • Admin/initialization functions: confidence reduced by 40%
  • Read-only/view functions: confidence reduced by 60%

Limitations

False positives:

  • PDA accounts that are validated by derivation (seeds + bump) rather than by explicit key/owner checks may be flagged, even though PDA derivation provides equivalent security.
  • Read-only reference accounts (e.g., price feeds, oracles) that only need key validation may be flagged for missing signer checks.

False negatives:

  • Accounts validated through variable expressions (e.g., HirExpr::Param) rather than simple variables are not tracked.
  • Validation performed in helper functions called from the main instruction handler is not recognized.

References