Severity: 🔴 CRITICAL
This example demonstrates one of the most dangerous and common vulnerabilities in Solana programs: Missing Signer Authorization.
When a program needs to verify that a specific account authorized an action, it must check that the account actually signed the transaction. Simply checking the account's public key is not enough!
This vulnerability has led to multiple exploits in production Solana programs, resulting in:
- Complete drainage of protocol funds
- Unauthorized access to privileged operations
- Loss of user assets
- ✅ Why checking public keys alone is insufficient
- ✅ The difference between
AccountInfoandSignertypes - ✅ How attackers exploit missing signer checks
- ✅ How to properly validate transaction signers in Anchor
This program contains a simple vault system with three instructions:
initialize- Creates a vault with an authoritydeposit- Deposits SOL into the vault (anyone can deposit)withdraw_vulnerable⚠️ - Vulnerable withdrawal (missing signer check)withdraw_secure✅ - Secure withdrawal (proper signer check)
Located in: src/instructions/withdraw_vulnerable.rs
#[derive(Accounts)]
pub struct WithdrawVulnerable<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
/// ⚠️ CRITICAL VULNERABILITY: Using AccountInfo instead of Signer
pub authority: AccountInfo<'info>, // ❌ WRONG!
#[account(mut)]
pub recipient: SystemAccount<'info>,
}
pub fn handler(ctx: Context<WithdrawVulnerable>, amount: u64) -> Result<()> {
// ⚠️ This check is INSUFFICIENT!
// We only verify the PUBLIC KEY matches, not that they SIGNED!
require!(
ctx.accounts.authority.key() == vault.authority,
ErrorCode::Unauthorized
);
// Transfer funds...
}The code checks authority.key() == vault.authority, which only verifies:
- ✅ The public key matches
But it does NOT verify:
- ❌ That the authority signed the transaction
- ❌ That the caller has the private key
- ❌ That this is an authorized action
- Alice creates a vault and deposits 10 SOL
- Bob (attacker) calls
withdraw_vulnerable:- Passes Alice's public key as the
authority(doesn't need her private key!) - Passes his own wallet as the
recipient
- Passes Alice's public key as the
- The check
authority.key() == vault.authoritypasses ✓ - Bob steals all 10 SOL! 💸
// Attacker doesn't need the victim's private key!
await program.methods
.withdrawVulnerable(new BN(10_000_000_000))
.accounts({
vault: victimVaultPDA,
authority: victimPublicKey, // Just the PUBLIC KEY!
recipient: attackerWallet.publicKey, // Attacker's wallet
})
.rpc();
// 💰 Funds stolen!Located in: src/instructions/withdraw_secure.rs
#[derive(Accounts)]
pub struct WithdrawSecure<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
/// ✅ SECURE: Using Signer type
#[account(
constraint = authority.key() == vault.authority @ ErrorCode::Unauthorized
)]
pub authority: Signer<'info>, // ✅ CORRECT!
#[account(mut)]
pub recipient: SystemAccount<'info>,
}One simple change fixes the vulnerability:
- pub authority: AccountInfo<'info>, // ❌ Vulnerable
+ pub authority: Signer<'info>, // ✅ SecureWhen using Signer<'info>:
- Anchor automatically verifies the account signed the transaction
- If the signature is missing, the transaction fails immediately
- Attackers cannot pass a public key without the private key
- Funds are safe! ✅
If you must use AccountInfo, you can manually verify:
pub authority: AccountInfo<'info>,
// In handler:
require!(
authority.is_signer,
ErrorCode::MissingRequiredSignature
);But using Signer<'info> is CLEANER and SAFER!
- Use
AccountInfofor accounts that must sign - Only check public keys for authorization
- Assume accounts are signers without verification
- Use
Signer<'info>for accounts that must sign transactions - Let Anchor handle signer verification automatically
- Always verify both identity AND signature
- Public Key Cryptography: Public keys are... public! Anyone can know them.
- Digital Signatures: Only the private key holder can create valid signatures.
- Transaction Signers: Solana requires all signers to be listed in the transaction.
# Navigate to the program directory
cd 0-missing-signer-check
# Build the program
anchor buildThis code is for educational purposes only. The vulnerable code is intentionally insecure to demonstrate security concepts.
Remember: In production, ALWAYS use Signer<'info> for accounts that must authorize actions!