Durable Nonce Manipulation Remediation
How to fix missing validation when interacting with durable nonce accounts.
Durable Nonce Manipulation Remediation
Overview
Related Detector: Durable Nonce Manipulation
Durable nonce accounts extend the validity window of Solana transactions beyond the standard blockhash expiry. When programs interact with nonce accounts without validating authority, state, and advancement, attackers can replay transactions or substitute unauthorized nonce accounts. The fix is to validate nonce account ownership, authority, and state before every nonce operation.
Recommended Fix
Before (Vulnerable)
pub fn use_nonce(accounts: &[AccountInfo]) -> ProgramResult {
let nonce_info = &accounts[2];
// No validation -- attacker can pass any account
invoke(
&system_instruction::advance_nonce_account(nonce_info.key, nonce_info.key),
&[nonce_info.clone()],
)?;
Ok(())
}
After (Fixed)
pub fn use_nonce(accounts: &[AccountInfo]) -> ProgramResult {
let nonce_info = &accounts[2];
let authority_info = &accounts[3];
// Step 1: Validate nonce account is owned by System Program
if nonce_info.owner != &system_program::ID {
return Err(ProgramError::InvalidAccountOwner);
}
// Step 2: Validate authority is a signer
if !authority_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Step 3: Validate nonce account state is initialized
let nonce_data = nonce_account::state_from_account(nonce_info)?;
if nonce_data.state != nonce::State::Initialized {
return Err(ProgramError::InvalidAccountData);
}
// Step 4: Validate authority matches expected value
if nonce_data.authority != *authority_info.key {
return Err(ProgramError::InvalidArgument);
}
// Step 5: Advance nonce with validated authority
invoke(
&system_instruction::advance_nonce_account(nonce_info.key, authority_info.key),
&[nonce_info.clone(), authority_info.clone()],
)?;
Ok(())
}
Alternative Mitigations
1. Use recent blockhash instead
For most programs, standard recent blockhash transaction expiry is sufficient and avoids nonce complexity entirely:
// No nonce needed -- transactions expire naturally after ~150 slots
pub fn process(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
// Standard transaction processing
Ok(())
}
2. Wrap nonce validation in a helper
fn validate_nonce_account(
nonce_info: &AccountInfo,
expected_authority: &Pubkey,
) -> ProgramResult {
if nonce_info.owner != &system_program::ID {
return Err(ProgramError::InvalidAccountOwner);
}
let state = nonce_account::state_from_account(nonce_info)?;
if state.authority != *expected_authority {
return Err(ProgramError::InvalidArgument);
}
Ok(())
}
Common Mistakes
Mistake 1: Trusting the nonce account key without owner check
// WRONG: Anyone can create an account with arbitrary data
if nonce_info.key == &expected_nonce_pubkey {
// Key match does not guarantee System Program ownership
}
Always verify nonce_info.owner == &system_program::ID in addition to key checks.
Mistake 2: Not advancing the nonce after use
// WRONG: Nonce is validated but never advanced
let nonce_data = nonce_account::state_from_account(nonce_info)?;
// Process transaction without advancing nonce...
// The same nonce value can be reused in a replay attack
Always advance the nonce as part of the transaction to invalidate the current value.
Mistake 3: Using nonce authority as the nonce account key
// WRONG: Assumes authority == nonce account
invoke(
&system_instruction::advance_nonce_account(nonce_info.key, nonce_info.key),
&[nonce_info.clone()],
)?;
The authority and nonce account are separate entities. Always pass the correct authority.