Missing Owner Check Remediation
How to add proper account ownership validation in Solana programs to prevent unauthorized access via accounts owned by other programs.
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)
Anchor’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