General
Program Security

Program Security

Reinitialization Attacks

Reinitialization attacks exploit programs that fail to check whether an account has already been initialized, allowing attackers to overwrite existing data and hijack control of valuable accounts.

While initialization legitimately sets up new accounts for first-time use, reinitialization maliciously resets existing accounts to attacker-controlled states.

Without proper initialization validation, attackers can call initialization functions on accounts that are already in use, effectively performing a hostile takeover of established program state. This is particularly devastating in protocols like escrows, vaults, or any system where account ownership determines control over valuable assets.

Initialization sets the data of a new account for the first time. It's essential to check if an account has already been initialized to prevent overwriting existing data.

Anchor

Consider this vulnerable instruction that initializes a program account:

#[program]
pub mod unsafe_initialize_account{
    use super::*;
 
    //..
 
    pub fn unsafe_initialize_account(ctx: Context<InitializeAccount>) -> Result<()> {
        let mut writer: Vec<u8> = vec![];
 
        ProgramAccount {
            owner: ctx.accounts.owner.key()
        }.try_serialize(&mut writer)?;
 
        let mut data = ctx.accounts.program_account.try_borrow_mut_data()?;
        sol_memcpy(&mut data, &writer, writer.len());
 
        Ok(())
    }
 
    //..
}
 
#[derive(Accounts)]
pub struct InitializeAccount<'info> {
    pub owner: Signer<'info>,
    #[account(mut)]
    /// CHECK: This account will not be checked by Anchor
    pub program_account: UncheckedAccount<'info>,
}
 
#[account]
pub struct ProgramAccount {
    owner: Pubkey,
}

This code has a fatal flaw: it never checks whether the account has already been initialized. Every time this instruction is called, it unconditionally overwrites the account data and sets the caller as the new owner, regardless of the account's previous state.

An attacker can exploit this by:

  • Identifying a valuable initialized account (like an escrow PDA controlling token accounts)
  • Calling unsafe_initialize_account with that existing account
  • Becoming the new "owner" by overwriting the previous owner data
  • Using their newfound ownership to drain any assets controlled by that account

This attack is particularly devastating in escrow scenarios. Imagine an escrow PDA that owns token accounts containing thousands of dollars worth of assets. The original escrow initialization properly set up the account with legitimate participants. But if an attacker can call the reinitialization function, they can overwrite the escrow data, set themselves as the owner, and gain control over all the escrowed tokens.

Luckily Anchor makes it super easy to perform this check directly in the account struct by just using the init constraint when initializing the account like this:

#[derive(Accounts)]
pub struct InitializeAccount<'info> {
    pub owner: Signer<'info>,
    #[account(
        init,
        payer = owner,
        space = 8 + ProgramAccount::INIT_SPACE
    )]
    pub program_account: Account<'info, ProgramAccount>,
}
 
#[account]
#[derive(InitSpace)]
pub struct ProgramAccount {
    owner: Pubkey,
}

Or you could just check that the account has already been initialized in the instruction using the ctx.accounts.program_account.is_initialized check like this:

pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
    if ctx.accounts.program_account.is_initialized {
        return Err(ProgramError::AccountAlreadyInitialized.into());
    }
 
    Ok(())
}

Anchor's init_if_needed constraint, guarded by a feature flag, should be used with extreme caution. While it conveniently initializes an account only if it hasn't been initialized yet, it creates a dangerous trap: if the account is already initialized, the instruction handler continues executing normally. This means your program could unknowingly operate on existing accounts, potentially overwriting critical data or allowing unauthorized access.

Pinocchio

In Pinocchio, since we don't have the possibility of adding security checks directly inside of the account struct, we are forced to do so in the instruction logic.

We can do so by checking if the account has the correct discriminator:

let account_data = self.accounts.program_account.try_borrow_data()?;
 
if account_data[0] == DISCRIMINATOR {
    return Err(ProgramError::AccountAlreadyInitialized.into());
}
Contents
View Source
Blueshift © 2025Commit: e508535