Skip to main content
Sigvex

Sysvar Account Spoofing Remediation

How to prevent sysvar spoofing by validating account keys or using the Sysvar::get() API.

Sysvar Account Spoofing Remediation

Overview

Related Detector: Sysvar Account Spoofing

Sysvar spoofing vulnerabilities arise when a program reads system data from an account without verifying that the account is the real sysvar. The simplest fix is to use Sysvar::get() which reads directly from the runtime. When an account-based read is necessary, validate the account key against the canonical sysvar address.

Before (Vulnerable)

pub fn check_timelock(accounts: &[AccountInfo]) -> ProgramResult {
    let clock_account = &accounts[2];

    // VULNERABLE: no key validation
    let data = clock_account.try_borrow_data()?;
    let unix_timestamp = i64::from_le_bytes(data[32..40].try_into().unwrap());

    if unix_timestamp < unlock_time {
        return Err(ProgramError::InvalidArgument);
    }
    Ok(())
}

After (Fixed)

use solana_program::sysvar::clock::Clock;
use solana_program::sysvar::Sysvar;

pub fn check_timelock(_accounts: &[AccountInfo]) -> ProgramResult {
    // FIXED: Sysvar::get() reads from the runtime -- no spoofable account
    let clock = Clock::get()?;

    if clock.unix_timestamp < unlock_time {
        return Err(ProgramError::InvalidArgument);
    }
    Ok(())
}

Alternative Mitigations

1. Validate sysvar account key

When you need to pass the sysvar as an account (e.g., for CPI or for legacy compatibility):

use solana_program::sysvar;

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let clock_account = &accounts[2];

    // Validate key matches canonical Clock sysvar
    if clock_account.key != &sysvar::clock::ID {
        return Err(ProgramError::InvalidAccountData);
    }

    let clock = Clock::from_account_info(clock_account)?;
    // Safe to use clock.unix_timestamp
    Ok(())
}

2. Use Sysvar::from_account_info() (validates internally)

use solana_program::sysvar::Sysvar;

let clock = Clock::from_account_info(clock_account)?;  // Validates key internally
let rent = Rent::from_account_info(rent_account)?;

This method checks the account key and owner before deserializing.

3. Anchor Sysvar<'info, T> type

Anchor validates the sysvar account automatically:

#[derive(Accounts)]
pub struct ProcessTimelock<'info> {
    pub clock: Sysvar<'info, Clock>,  // Validated at deserialization
}

pub fn process_timelock(ctx: Context<ProcessTimelock>) -> Result<()> {
    let timestamp = ctx.accounts.clock.unix_timestamp;
    // Safe -- Anchor validated the account
    Ok(())
}

Common Mistakes

Mistake 1: Checking owner but not key

// WRONG: System program owns many accounts, not just sysvars
if clock_account.owner != &solana_program::system_program::ID {
    return Err(ProgramError::InvalidAccountData);
}

Sysvars are owned by the Sysvar program (Sysvar1111111111111111111111111111111111111), but checking owner alone is insufficient. Check the specific sysvar key.

Mistake 2: Hardcoding sysvar data offsets without key validation

// Correctly reads the right offsets, but from a fake account
let slot = u64::from_le_bytes(data[0..8].try_into().unwrap());
let epoch = u64::from_le_bytes(data[16..24].try_into().unwrap());

Correct offset parsing is irrelevant if the data source is attacker-controlled. Always validate the account key first.

Mistake 3: Using Sysvar::get() for Instructions sysvar

// WRONG: Instructions sysvar does NOT support Sysvar::get()
// It must be passed as an account and key-validated
let instructions = Instructions::get()?;  // This will fail

The Instructions sysvar requires an account because its data depends on the current transaction context. Use instructions::load_instruction_at_checked() with a key-validated account.

References