Missing Empty Account Check
Detects account initialization without validating the account is empty first.
Missing Empty Account Check
Overview
Remediation Guide: How to Fix Missing Empty Account Check
The missing empty account check detector identifies Solana program functions that write to account data at offset zero (discriminator/initialization area) without first verifying the account is empty or uninitialized. When this guard is absent, an attacker can pass an already-initialized account into an initialization handler, bypassing setup-time security checks or corrupting existing program state.
Sigvex detects this by scanning for StoreAccountData writes at low offsets (0 through 8) and verifying that a preceding data-length check, first-byte comparison, or CheckKey validation exists on every path to the write.
Why This Is an Issue
Failing to verify that an account is empty before initialization opens two primary attack surfaces:
- Re-initialization bypass: the attacker calls the init function on an already-initialized account to overwrite the authority, nonce, or balance fields with attacker-controlled values.
- State corruption: overwriting structured data with initialization values destroys existing state, potentially unlocking funds, resetting counters, or invalidating audit trails.
The vulnerability is analogous to writing a file without checking whether it already exists. In the Solana runtime, accounts are passed in by callers, so a program cannot assume accounts arrive in a specific state without explicit validation.
CWE mapping: CWE-665 (Improper Initialization).
How to Resolve
Native Solana
pub fn initialize(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
let state_account = &accounts[0];
let data = state_account.data.borrow();
// FIXED: Verify account is empty before initialization
if !data.is_empty() && data[0] != 0 {
return Err(ProgramError::AccountAlreadyInitialized);
}
drop(data);
let mut data = state_account.data.borrow_mut();
data[0..8].copy_from_slice(b"mydisc01");
data[8..40].copy_from_slice(authority.as_ref());
Ok(())
}
Anchor
#[derive(Accounts)]
pub struct Initialize<'info> {
// Anchor's init constraint handles this automatically:
// - Requires account to be system-owned (uninitialized)
// - Allocates space, sets discriminator, transfers ownership
#[account(init, payer = user, space = 8 + 32)]
pub state: Account<'info, MyState>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
Examples
Vulnerable
pub fn initialize(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
let account = &accounts[0];
let mut data = account.data.borrow_mut();
// No check whether account is already initialized
data[0..8].copy_from_slice(b"mydisc01");
data[8..40].copy_from_slice(authority.as_ref());
Ok(())
}
Fixed
pub fn initialize(accounts: &[AccountInfo], authority: Pubkey) -> ProgramResult {
let account = &accounts[0];
let data = account.data.borrow();
if data[0..8] != [0u8; 8] {
return Err(ProgramError::AccountAlreadyInitialized);
}
drop(data);
let mut data = account.data.borrow_mut();
data[0..8].copy_from_slice(b"mydisc01");
data[8..40].copy_from_slice(authority.as_ref());
Ok(())
}
JSON Finding
{
"detector": "missing-empty-account-check",
"severity": "High",
"confidence": 0.75,
"title": "Missing Empty Account Check Before Initialization",
"description": "Account is being initialized (write at offset 0) without first checking that it is empty.",
"cwe": [665]
}
Detection Methodology
The detector performs a two-pass analysis over the function’s HIR (High-level Intermediate Representation). The first pass collects accounts that have been validated as empty through data-length comparisons, first-byte equality checks, or CheckKey statements. The second pass identifies StoreAccountData writes targeting offsets 0 through 8 and reports any writes where the target account was not previously validated. Confidence is reduced for Anchor programs (which handle this via the init constraint) and for admin/initialization functions that are likely caller-protected.
Limitations
- The detector uses intraprocedural analysis and cannot track validation that occurs in a different function or across CPI boundaries.
- Writes to offsets above 8 are not flagged, even if they represent initialization without a guard.
- Anchor programs using
init_if_neededmay still be vulnerable despite being an Anchor program.
Related Detectors
- Account Reinitialization - detects re-initialization when a discriminator check is missing.
- Generic Init Frontrun - detects frontrunnable initialization patterns.
- Uninitialized Account - detects reads from uninitialized accounts.