Skip to main content
Sigvex

Missing Signer Check Remediation

How to add proper signer verification before privileged operations in Solana programs using both native and Anchor approaches.

Missing Signer Check Remediation

Overview

Related Detector: Missing Signer Check

A missing signer check allows any caller to perform privileged operations — transferring funds, modifying state — by passing an arbitrary account without needing its cryptographic signature. The fix is to verify account.is_signer before any transfer or state write involving that account.

Before (Vulnerable)

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

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

    // VULNERABLE: no is_signer check — any caller can drain the vault
    **vault.lamports.borrow_mut() -= amount;
    **authority.lamports.borrow_mut() += amount;
    Ok(())
}

After (Fixed)

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    program_error::ProgramError,
    msg,
};

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

    // FIXED: require the authority account to have signed this transaction
    if !authority.is_signer {
        msg!("Authority account must sign the transaction");
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Optional: also verify the authority matches expected key
    if authority.key != &EXPECTED_AUTHORITY {
        return Err(ProgramError::InvalidAccountData);
    }

    **vault.lamports.borrow_mut() -= amount;
    **authority.lamports.borrow_mut() += amount;
    Ok(())
}

The fix adds a simple is_signer check before any fund movement. This ensures the Solana runtime has verified a valid cryptographic signature from the authority account’s private key.

Alternative Mitigations

1. Anchor #[account(signer)] or Signer<'info> type

The idiomatic Anchor approach — the framework checks is_signer automatically at account deserialization:

use anchor_lang::prelude::*;

#[derive(Accounts)]
pub struct Withdraw<'info> {
    // Signer<'info> type automatically validates is_signer at deserialization
    pub authority: Signer<'info>,

    #[account(
        mut,
        has_one = authority,  // Also validates authority matches stored field
    )]
    pub vault: Account<'info, Vault>,
}

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    // authority.is_signer is guaranteed by Anchor — no manual check needed
    let vault = &mut ctx.accounts.vault;
    require!(vault.balance >= amount, MyError::InsufficientFunds);
    vault.balance -= amount;
    Ok(())
}

2. PDA signer via invoke_signed (when the authority is a PDA)

PDAs cannot sign transactions directly. Instead, use invoke_signed to grant them signing authority:

// The program itself signs on behalf of the PDA
let seeds = &[b"vault".as_ref(), owner.key.as_ref(), &[vault_bump]];
let signer_seeds = &[seeds.as_slice()];

let transfer_instruction = system_instruction::transfer(
    vault.key,
    recipient.key,
    amount,
);

invoke_signed(
    &transfer_instruction,
    &[vault.clone(), recipient.clone(), system_program.clone()],
    signer_seeds,
)?;

3. Authority chain validation

When multiple signers are involved, validate the complete authority chain:

pub fn admin_operation(accounts: &[AccountInfo]) -> ProgramResult {
    let admin = &accounts[0];
    let multisig = &accounts[1];

    // Primary signer
    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Verify admin is listed in the multisig
    let multisig_data = multisig.data.borrow();
    let admin_keys = parse_multisig_keys(&multisig_data);
    if !admin_keys.contains(admin.key) {
        return Err(MyError::NotAnAdmin.into());
    }

    // Proceed with admin operation
    Ok(())
}

Common Mistakes

Mistake 1: Checking signer after the operation

// WRONG: checking signer after the state modification
pub fn bad_order(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    **accounts[1].lamports.borrow_mut() -= amount; // State modified first!
    if !accounts[0].is_signer {
        return Err(ProgramError::MissingRequiredSignature); // Too late
    }
    Ok(())
}

Mistake 2: Checking signer but not matching against expected authority

// INCOMPLETE: anyone who signs can call this, not just the vault's authority
pub fn incomplete_check(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    if !accounts[0].is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    // Missing: accounts[0].key == vault.authority — any signer can withdraw
    **accounts[1].lamports.borrow_mut() -= amount;
    **accounts[0].lamports.borrow_mut() += amount;
    Ok(())
}

Mistake 3: Trusting UncheckedAccount in Anchor

// WRONG: UncheckedAccount bypasses the account-validation framework's automatic signer check
#[derive(Accounts)]
pub struct BadWithdraw<'info> {
    pub authority: UncheckedAccount<'info>,  // No signer check!
    // ...
}

Use Signer<'info> instead of UncheckedAccount<'info> when a signature is required.

References