Instruction Sender Validation Remediation
How to fix missing authority validation for transaction senders in privileged operations.
Instruction Sender Validation Remediation
Overview
Related Detector: Instruction Sender Validation
Missing instruction sender validation occurs when a program checks is_signer but does not verify that the signer’s public key matches an expected authority address. The fix requires two checks: (1) confirm the account signed the transaction, and (2) confirm the signer’s key matches the stored authority.
Recommended Fix
Before (Vulnerable)
pub fn admin_action(accounts: &[AccountInfo]) -> ProgramResult {
let caller = &accounts[0];
let target = &accounts[1];
// Only checks signer -- any signer can call
if !caller.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let mut data = target.try_borrow_mut_data()?;
data[0..8].copy_from_slice(&0u64.to_le_bytes());
Ok(())
}
After (Fixed)
pub fn admin_action(accounts: &[AccountInfo]) -> ProgramResult {
let caller = &accounts[0];
let target = &accounts[1];
let config = &accounts[2];
// Check 1: signer verification
if !caller.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Check 2: authority validation
let config_data = config.try_borrow_data()?;
let expected_authority = Pubkey::try_from(&config_data[0..32])
.map_err(|_| ProgramError::InvalidAccountData)?;
if *caller.key != expected_authority {
return Err(ProgramError::InvalidArgument);
}
let mut data = target.try_borrow_mut_data()?;
data[0..8].copy_from_slice(&0u64.to_le_bytes());
Ok(())
}
Alternative Mitigations
1. Anchor Signer + has_one
Anchor handles both checks declaratively:
#[derive(Accounts)]
pub struct AdminAction<'info> {
pub authority: Signer<'info>, // Enforces is_signer
#[account(mut, has_one = authority @ ErrorCode::Unauthorized)]
pub config: Account<'info, Config>,
#[account(mut)]
pub target: Account<'info, TargetState>,
}
2. PDA-derived authority
Use a PDA as the authority so there is no ambiguity about which key is authorized:
let (expected_authority, _bump) = Pubkey::find_program_address(
&[b"authority", config_account.key.as_ref()],
program_id,
);
if *caller.key != expected_authority {
return Err(ProgramError::InvalidArgument);
}
3. Guard function for reuse
fn require_authority(
signer: &AccountInfo,
config: &AccountInfo,
) -> ProgramResult {
if !signer.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let data = config.try_borrow_data()?;
let expected = Pubkey::try_from(&data[0..32])
.map_err(|_| ProgramError::InvalidAccountData)?;
if *signer.key != expected {
return Err(ProgramError::InvalidArgument);
}
Ok(())
}
Common Mistakes
Mistake 1: Treating is_signer as sufficient authorization
is_signer confirms the private key signed the transaction. It does not confirm identity. Any wallet can sign a transaction, so you must also compare the public key.
Mistake 2: Comparing against a dynamic value from untrusted input
// WRONG: expected_key comes from instruction data (attacker-controlled)
let expected_key = Pubkey::try_from(&instruction_data[0..32])?;
if *signer.key != expected_key { return Err(...); }
The expected authority must come from a trusted on-chain account, not from instruction data.
Mistake 3: Validating the wrong account
Ensure the account you check is_signer on is the same account you compare against the authority. Checking signer on account A but comparing key on account B is not a valid validation.