Skip to main content
Sigvex

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:

  1. Ownership check collection: Scans all blocks for CheckOwner and CheckKey statements and records which blocks contain ownership validation.
  2. 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.
  3. 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.
  4. Predecessor analysis: For each privileged operation, walks the predecessor chain to verify that an ownership check dominates the operation block.
  5. 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.

References