Account Verification Chain Remediation
How to fix incomplete account verification chains by adding missing validation checks.
Remediating Incomplete Account Verification Chains
Overview
Related Detector: Account Verification Chain
Incomplete verification chains occur when an account has some validation checks (signer, owner, key) but not all that are required for its security role. The fix is to add the missing checks appropriate for each account’s purpose.
Recommended Fix
Before (Vulnerable)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let authority = &accounts[0];
let vault = &accounts[1];
// Only signer check on authority -- missing key and owner checks
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Only key check on vault -- missing owner check
if vault.key != &VAULT_ADDRESS {
return Err(ProgramError::InvalidArgument);
}
**vault.lamports.borrow_mut() -= amount;
**authority.lamports.borrow_mut() += amount;
Ok(())
}
After (Fixed)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
const AUTHORITY_KEY: Pubkey = /* expected authority */;
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let authority = &accounts[0];
let vault = &accounts[1];
// Complete chain: signer + key for authority
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if authority.key != &AUTHORITY_KEY {
return Err(ProgramError::InvalidArgument);
}
// Complete chain: key + owner for vault
if vault.key != &VAULT_ADDRESS {
return Err(ProgramError::InvalidArgument);
}
if vault.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
**vault.lamports.borrow_mut() -= amount;
**authority.lamports.borrow_mut() += amount;
Ok(())
}
Alternative Mitigations
- Anchor typed accounts: Use Anchor’s typed account system, which enforces owner, discriminator, and constraint validation automatically.
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(constraint = authority.key() == AUTHORITY_KEY)]
pub authority: Signer<'info>,
#[account(mut, constraint = vault.key() == VAULT_ADDRESS)]
pub vault: Account<'info, Vault>,
}
- PDA derivation: For accounts derived from seeds, validate using PDA derivation instead of explicit key checks. PDA derivation implicitly validates the owner (creating program) and key (deterministic from seeds).
let (expected_pda, _bump) = Pubkey::find_program_address(
&[b"vault", authority.key.as_ref()],
program_id,
);
if vault.key != &expected_pda {
return Err(ProgramError::InvalidSeeds);
}
- Validation helper function: Create a reusable validation function that enforces the full chain for common account patterns.
Common Mistakes
- Adding unnecessary checks: Not every account needs all three checks. PDAs need key (derivation) + owner but not signer. Authority accounts need signer + key but may not need owner. Understand the security role before adding checks.
- Checking the wrong owner: The owner of a program-created account is the creating program, not the user. Verify
account.owner == &MY_PROGRAM_ID, not the user’s public key. - Ordering checks incorrectly: Signer checks should come before expensive operations. Key and owner checks should come before any data access. Fail fast on the cheapest checks.
- Validating key against a mutable reference: If the expected key is stored in another account that could be attacker-controlled, the validation chain is only as strong as the weakest link.