Conditional Ownership Bypass
Detects ownership checks that can be bypassed through conditional logic, allowing unauthorized access.
Conditional Ownership Bypass
Overview
Remediation Guide: How to Fix Conditional Ownership Bypass
The conditional ownership bypass detector identifies Solana programs where ownership checks are applied inconsistently across conditional branches. When an ownership validation exists in one branch but not the other, or when a branch condition itself is the ownership check with privileged operations in both outcomes, an attacker can take the unchecked path to bypass authorization. This vulnerability appears in 10-15% of Solana audits and can lead to unauthorized account access, privilege escalation, and fund theft.
Why This Is an Issue
Ownership validation must execute on every path that reaches a privileged operation. When ownership checks are conditional:
- An attacker crafts input to direct execution into the unchecked branch
- Privileged operations (lamport transfers, CPI calls, state writes) execute without verifying that the caller owns the account
- Using the ownership check as a branch condition means the false branch executes without validation
- The program appears correct under normal usage but is exploitable under adversarial conditions
CWE mapping: CWE-863 (Incorrect Authorization).
How to Resolve
Native Solana
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
pub fn process(
accounts: &[AccountInfo],
program_id: &Pubkey,
flag: u8,
) -> Result<(), ProgramError> {
let user = &accounts[0];
let vault = &accounts[1];
// FIXED: ownership check BEFORE any branching
if vault.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
// Now both branches are safe
if flag == 1 {
**vault.try_borrow_mut_lamports()? -= 100;
**user.try_borrow_mut_lamports()? += 100;
} else {
**vault.try_borrow_mut_lamports()? -= 50;
**user.try_borrow_mut_lamports()? += 50;
}
Ok(())
}
Anchor
#[derive(Accounts)]
pub struct Transfer<'info> {
pub user: Signer<'info>,
#[account(
mut,
// Ownership enforced unconditionally by Anchor constraint
has_one = owner @ ErrorCode::Unauthorized
)]
pub vault: Account<'info, Vault>,
pub owner: AccountInfo<'info>,
}
Examples
Vulnerable Code
pub fn withdraw(accounts: &[AccountInfo], program_id: &Pubkey, fast_mode: bool) -> ProgramResult {
let user = &accounts[0];
let vault = &accounts[1];
if fast_mode {
// Fast path: MISSING ownership check
**vault.try_borrow_mut_lamports()? -= 100;
**user.try_borrow_mut_lamports()? += 100;
} else {
// Normal path: ownership checked
if vault.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
**vault.try_borrow_mut_lamports()? -= 100;
**user.try_borrow_mut_lamports()? += 100;
}
Ok(())
}
Fixed Code
pub fn withdraw(accounts: &[AccountInfo], program_id: &Pubkey, fast_mode: bool) -> ProgramResult {
let user = &accounts[0];
let vault = &accounts[1];
// FIXED: ownership check is unconditional
if vault.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
if fast_mode {
**vault.try_borrow_mut_lamports()? -= 100;
} else {
**vault.try_borrow_mut_lamports()? -= 100;
}
**user.try_borrow_mut_lamports()? += 100;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "conditional-ownership-bypass",
"severity": "high",
"confidence": 0.65,
"title": "Inconsistent Ownership Validation Across Branches",
"description": "The function validates account ownership in one branch but not the other. The false branch (block 2) lacks ownership validation while the other branch (block 1) performs the check. This allows bypassing the check by taking the unchecked path.",
"location": { "function": "withdraw", "block": 0 },
"cwe": 863
}
Detection Methodology
The detector performs control flow graph analysis to identify inconsistent ownership validation:
- Ownership check collection: Scans all blocks for
CheckOwnerandCheckKeystatements and records which blocks contain ownership validation. - Branch analysis: For each conditional branch, examines both the true and false target blocks (and their successors) to determine whether ownership checks exist in each path.
- Condition inspection: When the branch condition itself is an ownership comparison (e.g.,
account.owner == expected), both branches are checked for privileged operations — if both have them, the false branch is unprotected. - Predecessor analysis: For each privileged operation, walks the predecessor chain to verify that an ownership check dominates the operation block.
- Context adjustment: Confidence is reduced for Anchor programs (where
#[account]constraints validate ownership before the handler) and for read-only functions.
Limitations
False positives:
- Anchor programs where ownership is validated by account constraints before the handler executes.
- Programs where the unchecked branch contains only non-privileged operations (reads, logging).
False negatives:
- Ownership checks that depend on complex multi-step data flow across instructions.
- Dynamic ownership patterns where the expected owner is loaded from a separate account.
Related Detectors
- Conditional Validation Bypass — signer/key validations inside conditional branches
- Missing Owner Check — entirely missing ownership validation
- Authority Chain Validation — broken authority delegation chains