Skip to main content
Sigvex

Missing Owner Check

Detects account data reads where the account's program owner is not verified, allowing attackers to pass maliciously crafted accounts with arbitrary data.

Missing Owner Check

Overview

Remediation Guide: How to Fix Missing Owner Check

The missing owner check detector identifies Solana program functions that read account data — trusting its contents for program logic — without first verifying that the account is owned by the expected program. Each Solana account has an owner field that identifies which program controls its data. Without verifying ownership, an attacker can create an account owned by a different program (or a program they control) whose data layout mimics the expected structure, causing the vulnerable program to accept fabricated state.

Sigvex uses CFG-based dataflow analysis (v2.0) to track CheckOwner and CheckKey validations across basic blocks. The detector reports accounts whose data is read (HirExpr::AccountData) without a preceding owner or key validation on any reachable data-flow path.

Why This Is an Issue

Account owner verification is a fundamental security requirement in Solana programs. The Solana runtime does not automatically validate that accounts passed to a program are owned by that program — this check is the program’s responsibility. An attacker who can pass a maliciously crafted account as a data source can inject arbitrary values: fake balances, fraudulent authority public keys, or false configuration values.

This vulnerability class is distinct from but related to type cosplay: type cosplay concerns the account type discriminator, while missing owner check concerns the program-level ownership. Both must be validated; owner checks prevent injection from external programs, while discriminator checks prevent injection from within the same program.

CWE mapping: CWE-284 (Improper Access Control).

How to Resolve

// Before: Vulnerable — reads data without owner check
pub fn process_config(accounts: &[AccountInfo]) -> ProgramResult {
    let config = &accounts[0];
    let data = config.data.borrow();
    // VULNERABLE: no check that config.owner == &crate::id()
    // Attacker passes a config account they created with a different program
    let fee_rate = u64::from_le_bytes(data[8..16].try_into().unwrap());
    // fee_rate is attacker-controlled — could be 0 or MAX to manipulate protocol
    Ok(())
}

// After: Verify owner before trusting data
pub fn process_config(accounts: &[AccountInfo]) -> ProgramResult {
    let config = &accounts[0];

    // FIXED: verify account is owned by this program
    if config.owner != &crate::id() {
        return Err(ProgramError::IncorrectProgramId);
    }

    let data = config.data.borrow();
    let fee_rate = u64::from_le_bytes(data[8..16].try_into().unwrap());
    Ok(())
}

For Anchor:

// Anchor's Account<'info, T> automatically checks:
// 1. account.owner == program_id
// 2. discriminator bytes match T's type discriminator
#[derive(Accounts)]
pub struct ProcessConfig<'info> {
    pub config: Account<'info, Config>,  // Owner and discriminator auto-checked
}

Examples

Vulnerable Code

pub fn update_user_balance(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let user_state = &accounts[0];  // Expected: UserState account
    let data = user_state.data.borrow();

    // VULNERABLE: no owner check — attacker creates fake UserState with inflated balance
    let current_balance = u64::from_le_bytes(data[8..16].try_into()?);
    let authority = Pubkey::from_slice(&data[16..48])?;

    if authority != *accounts[1].key {
        return Err(MyError::Unauthorized.into());
    }

    // With attacker's fake account, current_balance and authority are arbitrary
    apply_update(user_state, amount)?;
    Ok(())
}

Fixed Code

pub fn update_user_balance(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let user_state = &accounts[0];

    // FIXED: must be owned by this program
    if user_state.owner != &crate::id() {
        msg!("Account not owned by this program");
        return Err(ProgramError::IncorrectProgramId);
    }

    let data = user_state.data.borrow();
    let current_balance = u64::from_le_bytes(data[8..16].try_into()?);
    let authority = Pubkey::from_slice(&data[16..48])?;

    if authority != *accounts[1].key {
        return Err(MyError::Unauthorized.into());
    }

    apply_update(user_state, amount)?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "missing-owner-check",
  "severity": "critical",
  "confidence": 0.80,
  "description": "Account v0 data is read without verifying the account's owner. An attacker could pass a maliciously crafted account with arbitrary data to exploit the program.",
  "location": { "function": "update_user_balance", "offset": 2 }
}

Detection Methodology

The detector runs CFG dataflow analysis, tracking CheckOwner and CheckKey validations:

  1. Early exit: Functions with no AccountData reads or StoreAccountData writes are skipped.
  2. Dataflow initialization: DataflowAnalyzer::analyze() propagates validation states from predecessor blocks.
  3. Validation tracking: CheckOwner statements mark the account as owner-validated. CheckKey statements are treated as implicitly validating ownership (a specific key implies a specific owner).
  4. Data access detection: HirStmt::Assign with HirExpr::AccountData source without prior owner/key validation generates a finding.
  5. Confidence calculation:
    • No validation, no PDA: confidence 0.80
    • PDA-derived account: confidence 0.30 (PDAs are owned by the deriving program)
    • Discriminator check within 5 statements (lookahead): confidence 0.55
  6. Context modifiers: Anchor programs with discriminator validation reduce by 0.15x; without by 0.30x. Admin functions and read-only functions receive reduced confidence.

Limitations

False positives:

  • Accounts that are validated via a PDA derivation that implicitly guarantees ownership are marked PDA-derived and receive low confidence (0.30).
  • Anchor Account<'info, T> performs automatic owner validation — Anchor programs receive significantly reduced confidence.
  • Programs where ownership is checked in a preceding instruction (guarding the entire instruction set) may be flagged for inner functions.

False negatives:

  • Owner checks performed in a separate validation helper function (not inlined) are not recognized.
  • Programs that validate ownership through program-level constraints at the account loading stage may not have visible owner checks in individual instruction handlers.

References