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:
- Early exit: Functions with no
AccountDatareads orStoreAccountDatawrites are skipped. - Dataflow initialization:
DataflowAnalyzer::analyze()propagates validation states from predecessor blocks. - Validation tracking:
CheckOwnerstatements mark the account as owner-validated.CheckKeystatements are treated as implicitly validating ownership (a specific key implies a specific owner). - Data access detection:
HirStmt::AssignwithHirExpr::AccountDatasource without prior owner/key validation generates a finding. - 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
- 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.
Related Detectors
- Missing Signer Check — validates transaction signatures before operations
- Type Cosplay — validates discriminator before deserialization