Missing Signer Check Exploit Generator
Sigvex exploit generator that validates missing signer check vulnerabilities in Solana programs by simulating instruction execution without required signatures, confirming unauthorized access to protected program logic.
Missing Signer Check Exploit Generator
Overview
The missing signer check exploit generator validates findings from the missing-signer-check detector by simulating an instruction call to the target program without the required signer account. If the program processes the instruction without rejecting it, the vulnerability is confirmed with confidence 0.95.
In Solana programs, every transaction specifies which accounts are signers. A program that does not verify account.is_signer before executing sensitive operations allows any caller to perform protected actions — withdrawals, admin updates, account closures — without authorization. Unlike Ethereum’s msg.sender check, Solana’s account model requires explicit programmatic checks on each account’s signer flag. This is one of the most common Solana vulnerability classes and has led to repeated exploits across DeFi protocols.
Severity score: 90/100 (Critical).
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Admin function bypass:
- A Solana program has an
update_authorityinstruction that should only be callable by the current authority. - The instruction receives an
authorityaccount but does not checkctx.accounts.authority.is_signer. - An attacker constructs a transaction passing any public key as the
authorityaccount. - Since
is_signeris not verified, the program accepts the call. - The attacker replaces the authority with their own key.
- All subsequent admin operations are under attacker control.
Withdrawal authorization bypass:
- A token vault requires the beneficiary to sign before releasing funds.
- The program checks
beneficiary == expected_beneficiary(public key match) but notbeneficiary.is_signer. - An attacker passes the expected beneficiary’s public key as a non-signing account.
- The public key check passes, the signer check is missing.
- The attacker drains the vault without the beneficiary’s private key.
Exploit Mechanics
The engine maps the missing-signer-check detector to the missing signer exploit pattern (severity score 90/100).
Strategy: Construct a transaction that passes the expected authority account as a non-signing account. Because the program does not verify account.is_signer, the instruction executes successfully.
Simulation steps:
- The engine selects the target program and identifies the vulnerable instruction from the finding location.
- An exploit transaction is constructed with the authority account’s public key listed in the accounts array but with
is_signer = false. - The transaction is submitted to the program simulator.
- If the program processes the instruction and modifies authority-gated state (e.g., updating the authority field, transferring funds), the vulnerability is confirmed with confidence 0.95.
- The exploit result records:
status = Confirmed,description = "Unauthorized access to protected instructions",impact = "High".
Exploit transaction structure:
// Attacker constructs this transaction — no private key required
let accounts = vec![
AccountMeta::new(vault_pubkey, false), // vault account (writable)
AccountMeta::new_readonly(authority_pubkey, false), // authority as NON-SIGNER
// ^ is_signer=false but program does not check!
];
let ix = Instruction::new_with_bytes(
program_id,
&update_authority_discriminator, // Anchor 8-byte discriminator
accounts,
);
// Transaction succeeds without authority's signature
Evidence collected: The exploit generator records the authority public key used, the instruction discriminator targeted, and the resulting state change (new authority field value) as confirmation evidence.
// VULNERABLE: No is_signer check
use anchor_lang::prelude::*;
#[program]
mod vulnerable_vault {
pub fn update_authority(ctx: Context<UpdateAuthority>, new_authority: Pubkey) -> Result<()> {
// CRITICAL: Missing ctx.accounts.authority.is_signer check!
ctx.accounts.vault.authority = new_authority;
Ok(())
}
}
#[derive(Accounts)]
pub struct UpdateAuthority<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
/// CHECK: No signer constraint! Vulnerable!
pub authority: AccountInfo<'info>,
}
// ATTACK: Pass any key as authority
// ix = update_authority(accounts={vault, attacker_pubkey_as_authority}, new_authority=attacker_key)
// authority.is_signer = false — but the program doesn't check!
// SECURE: Require signer constraint
#[derive(Accounts)]
pub struct UpdateAuthoritySafe<'info> {
#[account(mut, has_one = authority)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>, // Anchor's Signer type enforces is_signer = true
}
// Or without Anchor:
pub fn update_authority(ctx: Context<UpdateAuthority>) -> ProgramResult {
let authority = &ctx.accounts.authority;
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// ... rest of logic
}
Remediation
- Detector: Missing Signer Check Detector
- Remediation Guide: Missing Signer Check Remediation
For Anchor programs, use the Signer<'info> type for any account that must authorize the transaction:
// Anchor: Signer<'info> enforces is_signer automatically
pub authority: Signer<'info>,
// Anchor constraint on account: has_one ensures authority matches stored key
#[account(mut, has_one = authority @ VaultError::Unauthorized)]
pub vault: Account<'info, Vault>,
For native programs:
if !authority_info.is_signer {
msg!("Authority account must be a signer");
return Err(ProgramError::MissingRequiredSignature);
}
Audit every instruction handler for accounts that control sensitive operations (fund transfers, configuration changes, account closures). Every such account must have an explicit is_signer check.