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.
Recommended Fix
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 Anchor'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.