Missing Owner Check Remediation
Overview
Related Detector: Missing Owner Check
Missing owner checks allow attackers to pass accounts owned by an arbitrary program, not the expected program. Since any program can create accounts, an attacker can craft a fake account with matching data layout but incorrect ownership. The fix is to verify account.owner before deserializing and using account data.
Recommended Fix
Before (Vulnerable)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn process_stake(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let stake_account = &accounts[0];
let data = stake_account.data.borrow();
// VULNERABLE: no owner check — attacker passes a fake account
// owned by their malicious program with crafted data
let authority = Pubkey::from_slice(&data[8..40])?;
let current_stake = u64::from_le_bytes(data[40..48].try_into().unwrap());
// Attacker controls authority and current_stake values
process_rewards(authority, current_stake)?;
Ok(())
}
After (Fixed)
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn process_stake(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let stake_account = &accounts[0];
// FIXED: verify account is owned by this program before reading data
if stake_account.owner != &crate::id() {
return Err(ProgramError::IncorrectProgramId);
}
let data = stake_account.data.borrow();
// Also validate the discriminator to prevent type cosplay
if data.len() < 8 || data[0..8] != STAKE_ACCOUNT_DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData);
}
let authority = Pubkey::from_slice(&data[8..40])?;
let current_stake = u64::from_le_bytes(data[40..48].try_into().unwrap());
process_rewards(authority, current_stake)?;
Ok(())
}
Alternative Mitigations
1. Anchor Account<'info, T> (Recommended)
the account-validation framework’s Account<'info, T> performs both owner and discriminator checks automatically:
use anchor_lang::prelude::*;
#[account]
pub struct StakeAccount {
pub authority: Pubkey, // offset 8
pub stake_amount: u64, // offset 40
pub rewards_earned: u64, // offset 48
}
#[derive(Accounts)]
pub struct ProcessStake<'info> {
// Account<'info, StakeAccount> validates:
// 1. stake_account.owner == program_id
// 2. discriminator == SHA256("account:StakeAccount")[0..8]
// 3. Deserializes safely
pub stake_account: Account<'info, StakeAccount>,
pub authority: Signer<'info>,
}
pub fn process_stake(ctx: Context<ProcessStake>, amount: u64) -> Result<()> {
// Owner and discriminator guaranteed by Anchor — safe to access fields
let stake = &ctx.accounts.stake_account;
process_rewards(stake.authority, stake.stake_amount)?;
Ok(())
}
2. Explicit Owner Check Helper
For native programs, create a reusable owner validation helper:
use solana_program::{account_info::AccountInfo, program_error::ProgramError};
/// Verify that an account is owned by the current program.
pub fn verify_program_owned(account: &AccountInfo) -> Result<(), ProgramError> {
if account.owner != &crate::id() {
return Err(ProgramError::IncorrectProgramId);
}
Ok(())
}
/// Verify that an account is owned by a specific expected program.
pub fn verify_owned_by(account: &AccountInfo, expected_owner: &Pubkey) -> Result<(), ProgramError> {
if account.owner != expected_owner {
return Err(ProgramError::IncorrectProgramId);
}
Ok(())
}
// Usage
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
verify_program_owned(&accounts[0])?;
// Safe to proceed
Ok(())
}
3. SPL Token Account Ownership
For token accounts, verify both program ownership and token mint:
use spl_token::state::Account as TokenAccount;
use solana_program::program_pack::Pack;
pub fn verify_token_account(
account: &AccountInfo,
expected_mint: &Pubkey,
expected_owner: &Pubkey,
) -> ProgramResult {
// Check program ownership (must be spl_token program)
if account.owner != &spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
// Unpack and verify token account fields
let token_account = TokenAccount::unpack(&account.data.borrow())?;
if &token_account.mint != expected_mint {
return Err(ProgramError::InvalidAccountData);
}
if &token_account.owner != expected_owner {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
Common Mistakes
Mistake 1: Checking Signer but Not Owner
// INSUFFICIENT: signer check prevents unauthorized callers but not
// fake accounts that happen to have a valid signature from the attacker
if !accounts[0].is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Still vulnerable: accounts[0] could be owned by attacker's program
let data = accounts[0].data.borrow();
Mistake 2: Using key() as a Proxy for Ownership
// WRONG: key match does not verify ownership
// An attacker can create an account AT the expected key if the key is predictable
if accounts[0].key != &expected_pubkey {
return Err(ProgramError::InvalidAccountData);
}
// Still vulnerable: if expected_pubkey is a known address the attacker can
// front-run account creation, ownership still belongs to attacker's program
Mistake 3: Checking Owner But Not Discriminator
// INCOMPLETE: owner check prevents cross-program attacks but not
// intra-program type cosplay (accounts of different types owned by the same program)
if accounts[0].owner != &crate::id() {
return Err(ProgramError::IncorrectProgramId);
}
// A Vault account owned by this program passes the check — but we expected StakeAccount
// Always also check the discriminator