Conditional Ownership Bypass Remediation
How to fix ownership checks that can be bypassed through conditional logic.
Conditional Ownership Bypass Remediation
Overview
Related Detector: Conditional Ownership Bypass
Conditional ownership bypass occurs when ownership validation is placed inside a conditional branch, allowing an attacker to reach privileged operations via the unchecked path. The fix is to move ownership validation above all conditional logic so it executes unconditionally before any privileged operation.
Recommended Fix
Before (Vulnerable)
pub fn process(accounts: &[AccountInfo], program_id: &Pubkey, mode: u8) -> ProgramResult {
let vault = &accounts[0];
let user = &accounts[1];
if mode == 1 {
// Ownership checked only on this path
if vault.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
**vault.try_borrow_mut_lamports()? -= 100;
**user.try_borrow_mut_lamports()? += 100;
} else {
// VULNERABLE: no ownership check
**vault.try_borrow_mut_lamports()? -= 50;
**user.try_borrow_mut_lamports()? += 50;
}
Ok(())
}
After (Fixed)
pub fn process(accounts: &[AccountInfo], program_id: &Pubkey, mode: u8) -> ProgramResult {
let vault = &accounts[0];
let user = &accounts[1];
// FIXED: ownership validated before any branching
if vault.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
let amount = if mode == 1 { 100 } else { 50 };
**vault.try_borrow_mut_lamports()? -= amount;
**user.try_borrow_mut_lamports()? += amount;
Ok(())
}
Alternative Mitigations
1. Anchor account constraints
Anchor validates ownership unconditionally before the handler runs:
#[derive(Accounts)]
pub struct Withdraw<'info> {
pub user: Signer<'info>,
#[account(mut, owner = crate::ID @ ErrorCode::InvalidOwner)]
pub vault: AccountInfo<'info>,
}
2. Guard function pattern
Extract ownership validation into a reusable guard that all code paths call:
fn validate_vault_ownership(vault: &AccountInfo, program_id: &Pubkey) -> ProgramResult {
if vault.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
Ok(())
}
pub fn process(accounts: &[AccountInfo], program_id: &Pubkey, mode: u8) -> ProgramResult {
let vault = &accounts[0];
validate_vault_ownership(vault, program_id)?;
// All branches are now safe
// ...
Ok(())
}
3. Separate instruction handlers
Split different modes into distinct instruction handlers, each with its own unconditional ownership check:
pub fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
match data[0] {
0 => process_standard_withdraw(program_id, accounts, data),
1 => process_priority_withdraw(program_id, accounts, data),
_ => Err(ProgramError::InvalidInstructionData),
}
}
Common Mistakes
Mistake 1: Using the ownership check as a branch condition
// WRONG: false branch has no ownership guarantee
if vault.owner == program_id {
do_privileged_action(vault)?;
} else {
do_privileged_action(vault)?; // Bypass!
}
If both branches perform privileged operations, validate ownership separately and unconditionally.
Mistake 2: Assuming attacker-controlled flags are trustworthy
// WRONG: attacker sets is_verified = true
if is_verified {
// skip check
} else {
check_owner(vault)?;
}
Never use caller-supplied parameters to conditionally skip security validation.
Mistake 3: Checking ownership only in the “main” path
Ensure all code paths — including error recovery, fallback, and edge-case paths — validate ownership before touching privileged account data.