Skip to main content
Sigvex

Conditional Validation Bypass

Detects security validations placed inside conditional branches that can be bypassed through alternate execution paths.

Conditional Validation Bypass

Overview

Remediation Guide: How to Fix Conditional Validation Bypass

The conditional validation bypass detector identifies Solana programs where critical security checks (signer validation, owner checks, key comparisons) are placed inside conditional branches rather than on the main execution path. When a validation is conditional, there exist execution paths that reach critical operations (transfers, CPI, state writes) without any validation having occurred. An attacker crafts input to take the non-validating branch.

This detector uses CFG-based dataflow analysis to identify execution paths from function entry to critical operations where validation blocks are not dominators – meaning the validation can be skipped.

Why This Is an Issue

Security validations are only effective if they execute on every path that reaches a protected operation. When a validation is inside a conditional:

  • An attacker can satisfy the branch condition to skip the validation block
  • The critical operation (transfer, mint, state write) executes without authorization
  • The program appears secure on some paths but is exploitable on others
  • This is especially dangerous when the branch condition depends on attacker-controlled input

This pattern is common in programs that handle multiple instruction types in a single function, where validation applies to some types but not all.

CWE mapping: CWE-862 (Missing Authorization), CWE-807 (Reliance on Untrusted Inputs in a Security Decision).

How to Resolve

// Before: Vulnerable -- signer check inside conditional branch
pub fn process(accounts: &[AccountInfo], instruction_type: u8) -> ProgramResult {
    let authority = &accounts[0];
    let vault = &accounts[1];

    if instruction_type == 1 {
        // Only checked on instruction_type == 1
        if !authority.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }
    }

    // VULNERABLE: reached without signer check when instruction_type != 1
    let mut data = vault.try_borrow_mut_data()?;
    let mut state = VaultState::try_from_slice(&data[8..])?;
    state.balance = 0;
    state.serialize(&mut &mut data[8..])?;
    Ok(())
}

// After: Validate before any critical operation
pub fn process(accounts: &[AccountInfo], instruction_type: u8) -> ProgramResult {
    let authority = &accounts[0];
    let vault = &accounts[1];

    // FIXED: signer check on all paths before state modification
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    if instruction_type == 1 {
        // Type-specific logic (non-security)
    }

    let mut data = vault.try_borrow_mut_data()?;
    let mut state = VaultState::try_from_slice(&data[8..])?;
    state.balance = 0;
    state.serialize(&mut &mut data[8..])?;
    Ok(())
}

Examples

Vulnerable Code

pub fn withdraw(accounts: &[AccountInfo], amount: u64, is_admin: bool) -> ProgramResult {
    let user = &accounts[0];
    let vault = &accounts[1];

    if is_admin {
        // Admin bypass -- no signer check on this path
    } else {
        // Normal user path -- validated
        if !user.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }
        if amount > MAX_USER_WITHDRAWAL {
            return Err(ProgramError::InvalidArgument);
        }
    }

    // CRITICAL: attacker sets is_admin = true, bypasses signer check
    **vault.try_borrow_mut_lamports()? -= amount;
    **user.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Fixed Code

pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let user = &accounts[0];
    let vault = &accounts[1];

    // FIXED: signer check is unconditional
    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // FIXED: validate user is vault owner
    let vault_data = vault.try_borrow_data()?;
    let vault_state = VaultState::try_from_slice(&vault_data[8..])?;
    if vault_state.owner != *user.key {
        return Err(ProgramError::InvalidAccountData);
    }

    if amount > vault_state.balance {
        return Err(ProgramError::InsufficientFunds);
    }

    drop(vault_data);
    **vault.try_borrow_mut_lamports()? -= amount;
    **user.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "conditional-validation-bypass",
  "severity": "critical",
  "confidence": 0.72,
  "description": "Signer validation at block 2 is inside a conditional branch. Block 5 contains a TransferLamports operation reachable via block 3 (bypass path) without passing through the validation. An attacker can take the bypass path to execute the transfer without authorization.",
  "location": { "function": "withdraw", "offset": 5 }
}

Detection Methodology

The detector performs CFG dominance analysis:

  1. Dataflow analysis: Runs DataflowAnalyzer to compute validation state at each block entry. This propagates CheckSigner, CheckOwner, and CheckKey validations across the CFG.
  2. Critical operation identification: Identifies blocks containing TransferLamports, InvokeCpi, or StoreAccountData statements.
  3. Bypass path detection: For each critical operation block, checks whether there exists a path from the function entry to that block where no validation has been applied. This is done by examining the dataflow entry state at the critical block.
  4. Conditional identification: When a validation exists in the function but does not dominate the critical operation (the validation is in a sibling branch rather than an ancestor), a bypass finding is emitted.
  5. Anchor adjustment: For Anchor programs, confidence is reduced by 0.70 because Anchor constraints execute unconditionally before the instruction handler, making conditional bypass within the handler less likely to be exploitable.

Limitations

False positives:

  • Programs where the conditional branch validates a different condition (e.g., “if amount > 0”) that functionally prevents the critical operation even without a signer check.
  • Anchor programs where constraints handle validation before the handler executes.

False negatives:

  • Validation that depends on complex data-flow conditions (e.g., checking a flag stored in account data that was set by a prior instruction).
  • Multi-instruction transaction patterns where validation occurs in a separate instruction.

References