Missing Signer Check
Detects privileged operations — lamport transfers and account data writes — performed without verifying that the involved account has signed the transaction.
Missing Signer Check
Overview
Remediation Guide: How to Fix Missing Signer Check
The missing signer check detector identifies Solana program functions that perform privileged operations — specifically lamport (SOL) transfers and account data writes — without first verifying that the account involved has signed the transaction. In Solana, any account can be passed to a program’s instruction; the program is responsible for verifying that sensitive accounts carry a valid signature (account.is_signer == true).
Sigvex uses CFG-based dataflow analysis (v2.0) to track signer checks across basic blocks, correctly handling cases where the check occurs in an earlier block than the privileged operation. The detector looks for HirStmt::TransferLamports and HirStmt::StoreAccountData operations where the source account has no preceding HirStmt::CheckSigner, HirStmt::CheckKey, or HirStmt::CheckOwner statement on the data-flow path.
Why This Is an Issue
Missing signer checks are among the most commonly exploited vulnerabilities in Solana programs. Because Solana programs receive a flat array of AccountInfo structures and do not inherently distinguish callers from data accounts, an attacker can pass any account in any position. Without a signer check, a program that transfers funds “from account[0]” will drain any account the attacker specifies — including accounts that hold significant SOL.
The Wormhole exploit ($325M, February 2022) exploited a missing signer verification: the program accepted a spoofed guardian_set account without verifying signatures, allowing the attacker to mint wrapped ETH without depositing real ETH.
CWE mapping: CWE-285 (Improper Authorization).
How to Resolve
// Before: Vulnerable — transfer without signer check
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_account = &accounts[0];
let vault = &accounts[1];
// VULNERABLE: no check that user_account.is_signer
// Attacker can pass any account as user_account
**vault.lamports.borrow_mut() -= amount;
**user_account.lamports.borrow_mut() += amount;
Ok(())
}
// After: Fixed — verify signer before privileged operation
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_account = &accounts[0];
let vault = &accounts[1];
// FIXED: verify the user must have signed this transaction
if !user_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
**vault.lamports.borrow_mut() -= amount;
**user_account.lamports.borrow_mut() += amount;
Ok(())
}
For Anchor programs:
// Anchor: use #[account(signer)] constraint
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(signer)] // Anchor auto-checks is_signer at deserialization
pub user: AccountInfo<'info>,
#[account(mut)]
pub vault: Account<'info, Vault>,
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// user.is_signer is guaranteed by the Anchor constraint above
ctx.accounts.vault.lamports -= amount;
Ok(())
}
Examples
Vulnerable Code
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program_error::ProgramError,
};
pub fn process_transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let from = next_account_info(accounts_iter)?;
let to = next_account_info(accounts_iter)?;
// MISSING SIGNER CHECK: anyone can drain 'from' by passing it as accounts[0]
**from.lamports.borrow_mut() -= amount;
**to.lamports.borrow_mut() += amount;
Ok(())
}
Fixed Code
pub fn process_transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let from = next_account_info(accounts_iter)?;
let to = next_account_info(accounts_iter)?;
// FIXED: the 'from' account must have signed this transaction
if !from.is_signer {
msg!("Missing required signature for source account");
return Err(ProgramError::MissingRequiredSignature);
}
**from.lamports.borrow_mut() -= amount;
**to.lamports.borrow_mut() += amount;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "missing-signer-check",
"severity": "critical",
"confidence": 0.85,
"description": "Account v1 is used in a privileged operation without verifying that it is a signer. An attacker could pass any account to drain funds or modify state.",
"location": { "function": "process_transfer", "offset": 2 }
}
Detection Methodology
The detector implements CFG-based dataflow analysis across basic blocks:
- Early exit: Skips functions with no
TransferLamportsorStoreAccountDatastatements to avoid unnecessary work. - Dataflow analysis: Runs
DataflowAnalyzer::analyze()to compute per-block entry states propagated from predecessor blocks. - PDA detection: Calls
collect_pda_derived_accounts()to identify accounts derived fromfind_program_address— PDAs cannot sign, so findings for PDA-derived accounts receive very low confidence (0.15). - Validation tracking: In each block, tracks
CheckSigner,CheckKey, andCheckOwnerstatements.CheckKeyreduces confidence to 0.45 (for transfers) since key validation provides partial protection.CheckOwnerreduces confidence to 0.55. - Finding generation: Reports findings with confidence 0.85 for bare unvalidated transfers.
- Context modifiers: Admin/initialization function names reduce confidence by
ADMIN_FUNCTION_CONFIDENCE_MULTIPLIER. Read-only function names reduce confidence by 0.40. Anchor programs with discriminator validation reduce by 0.15; without discriminator validation by 0.30. - Alias tracking:
HirStmt::Assignstatements are tracked for variable aliasing to handlelet a = b; check_signer(a)patterns.
Limitations
False positives:
- PDA accounts are correctly identified and receive very low confidence (0.15) since they are program-controlled.
- Admin functions that are protected at a higher level (e.g., the calling program checks the invoker’s authority before making the CPI) may still be flagged.
- Anchor programs using
Account<'info, T>with#[account(signer)]may be flagged if the discriminator check is not detected in the bytecode.
False negatives:
- Signer checks in called functions (not inlined) are not tracked without inter-procedural analysis.
- Programs where signer authority is delegated via CPI signer seeds (
invoke_signed) may not be recognized as having implicit authorization.
Related Detectors
- Missing Owner Check — detects missing
account.ownervalidation - Arbitrary CPI — detects unvalidated CPI target programs