Skip to main content
Sigvex

Instruction Sender Validation

Detects missing validation that the transaction sender has authority for privileged operations.

Instruction Sender Validation

Overview

Remediation Guide: How to Fix Instruction Sender Validation Issues

The instruction sender validation detector identifies Solana programs where privileged operations execute without verifying that the transaction signer has proper authority. This is distinct from a simple is_signer check – the detector specifically flags cases where the signer’s public key is not compared against an expected authority address. A program may verify that an account signed the transaction but still allow any signer to perform admin operations if it does not validate which signer is authorized.

Why This Is an Issue

On Solana, is_signer only confirms that the account signed the transaction. It does not confirm identity. Without checking the signer’s key against an expected authority:

  • Any wallet can execute privileged operations simply by signing the transaction
  • An attacker can drain funds by signing a transfer instruction with their own keypair
  • State modifications (configuration changes, parameter updates) are open to anyone
  • The program offers no meaningful access control despite having a signer check

This is one of the most common Solana vulnerabilities (CWE-862: Missing Authorization) and has led to significant fund losses in production programs.

How to Resolve

Native Solana

use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};

pub fn process_transfer(
    accounts: &[AccountInfo],
    amount: u64,
) -> Result<(), ProgramError> {
    let authority = &accounts[0];
    let treasury = &accounts[1];
    let config = &accounts[2];

    // Step 1: Verify signer status
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Step 2: Verify signer is the expected authority
    let config_data = config.try_borrow_data()?;
    let expected_authority = Pubkey::try_from(&config_data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if *authority.key != expected_authority {
        return Err(ProgramError::InvalidArgument);
    }

    // Safe: both signer and authority validated
    **treasury.try_borrow_mut_lamports()? -= amount;
    **authority.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct SecureTransfer<'info> {
    // Signer<'info> validates is_signer automatically
    // has_one validates the key matches config.authority
    pub authority: Signer<'info>,
    #[account(mut, has_one = authority @ ErrorCode::Unauthorized)]
    pub config: Account<'info, Config>,
    #[account(mut)]
    pub treasury: SystemAccount<'info>,
}

Examples

Vulnerable Code

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

    // Checks signer but NOT authority
    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // VULNERABLE: any signer can withdraw
    **treasury.try_borrow_mut_lamports()? -= amount;
    **user.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Fixed Code

pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let authority = &accounts[0];
    let treasury = &accounts[1];
    let config = &accounts[2];

    // Signer check
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Authority check
    let config_data = config.try_borrow_data()?;
    let expected = Pubkey::try_from(&config_data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if *authority.key != expected {
        return Err(ProgramError::InvalidArgument);
    }

    **treasury.try_borrow_mut_lamports()? -= amount;
    **authority.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "instruction-sender-validation",
  "severity": "critical",
  "confidence": 0.80,
  "title": "Missing Instruction Sender Authority Validation",
  "description": "Privileged operation (lamport transfer) on account v1 is performed without both signer verification and authority validation. An attacker could pass any account and execute unauthorized operations like draining funds or modifying state.",
  "location": { "function": "withdraw", "block": 0, "statement": 0 },
  "cwe": 862
}

Detection Methodology

The detector performs two-pass analysis over the function’s intermediate representation:

  1. First pass – validation tracking: Collects all CheckSigner statements (signer verification), CheckKey statements (authority validation), and bytecode-derived property reads (is_signer, account.key, account.owner) that serve as implicit validation.
  2. Privileged operation identification: Identifies TransferLamports and StoreAccountData operations as requiring both signer and authority validation.
  3. Gap analysis: For each privileged operation, checks whether the operating account has both a signer check and an authority check. Missing both checks yields a critical finding. Having only a signer check but no authority check yields a medium finding with reduced confidence (0.55), since signer-only may be valid for user-initiated operations.
  4. Context adjustment: Confidence is reduced for Anchor programs, admin/initialization functions, and read-only functions.

Limitations

False positives:

  • User-initiated operations where any signer is legitimately allowed (e.g., a user depositing into their own account). The detector reduces confidence for signer-only cases but may still flag them.
  • Anchor programs where Signer<'info> and has_one constraints handle validation.

False negatives:

  • Authority validation performed through CPI to a governance program.
  • Complex key derivation patterns where authority is validated indirectly via PDA seeds.

References