Skip to main content
Sigvex

Instruction Sender Validation Remediation

How to fix missing authority validation for transaction senders in privileged operations.

Instruction Sender Validation Remediation

Overview

Related Detector: Instruction Sender Validation

Missing instruction sender validation occurs when a program checks is_signer but does not verify that the signer’s public key matches an expected authority address. The fix requires two checks: (1) confirm the account signed the transaction, and (2) confirm the signer’s key matches the stored authority.

Before (Vulnerable)

pub fn admin_action(accounts: &[AccountInfo]) -> ProgramResult {
    let caller = &accounts[0];
    let target = &accounts[1];

    // Only checks signer -- any signer can call
    if !caller.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let mut data = target.try_borrow_mut_data()?;
    data[0..8].copy_from_slice(&0u64.to_le_bytes());
    Ok(())
}

After (Fixed)

pub fn admin_action(accounts: &[AccountInfo]) -> ProgramResult {
    let caller = &accounts[0];
    let target = &accounts[1];
    let config = &accounts[2];

    // Check 1: signer verification
    if !caller.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

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

    let mut data = target.try_borrow_mut_data()?;
    data[0..8].copy_from_slice(&0u64.to_le_bytes());
    Ok(())
}

Alternative Mitigations

1. Anchor Signer + has_one

an account-validation framework handles both checks declaratively:

#[derive(Accounts)]
pub struct AdminAction<'info> {
    pub authority: Signer<'info>,  // Enforces is_signer
    #[account(mut, has_one = authority @ ErrorCode::Unauthorized)]
    pub config: Account<'info, Config>,
    #[account(mut)]
    pub target: Account<'info, TargetState>,
}

2. PDA-derived authority

Use a PDA as the authority so there is no ambiguity about which key is authorized:

let (expected_authority, _bump) = Pubkey::find_program_address(
    &[b"authority", config_account.key.as_ref()],
    program_id,
);
if *caller.key != expected_authority {
    return Err(ProgramError::InvalidArgument);
}

3. Guard function for reuse

fn require_authority(
    signer: &AccountInfo,
    config: &AccountInfo,
) -> ProgramResult {
    if !signer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    let data = config.try_borrow_data()?;
    let expected = Pubkey::try_from(&data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if *signer.key != expected {
        return Err(ProgramError::InvalidArgument);
    }
    Ok(())
}

Common Mistakes

Mistake 1: Treating is_signer as sufficient authorization

is_signer confirms the private key signed the transaction. It does not confirm identity. Any wallet can sign a transaction, so you must also compare the public key.

Mistake 2: Comparing against a dynamic value from untrusted input

// WRONG: expected_key comes from instruction data (attacker-controlled)
let expected_key = Pubkey::try_from(&instruction_data[0..32])?;
if *signer.key != expected_key { return Err(...); }

The expected authority must come from a trusted on-chain account, not from instruction data.

Mistake 3: Validating the wrong account

Ensure the account you check is_signer on is the same account you compare against the authority. Checking signer on account A but comparing key on account B is not a valid validation.

References