Skip to main content
Sigvex

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.

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.

References