Summary
-
Prevent Account Reinitialization: Use an account discriminator or initialization flag to prevent an account from being reinitialized and overwriting existing data.
-
Anchor Approach: Simplify this by using Anchor's
init
constraint to create an account via a CPI to the system program, automatically setting its discriminator. -
Native Rust Approach: In native Rust, set an is_initialized flag during account initialization and check it before reinitializing:
Lesson
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. Note that creating and initializing an account are separate
actions. Creating an account involves invoking the create_account
instruction
handler on the System Program, which allocates space, rent in lamports, and
assigns the program owner. Initialization sets the account data. These steps can
be combined into a single transaction.
Missing Initialization Check
In the example below, there's no check on the user
account. The initialize
instruction handler sets the authority
field on the User
account type and
serializes the data. Without checks, an attacker could reinitialize the account,
overwriting the existing authority
.
Add is_initialized Check
To fix this, add an is_initialized
field to the User account type and check it
before reinitializing:
This ensures the user
account is only initialized once. If is_initialized
is
true, the transaction fails, preventing an attacker from changing the account
authority.
Use Anchor's init Constraint
Anchor's init
constraint,
used with the #[account(...)]
attribute, initializes an account, sets the
account discriminator, and ensures that the instruction handler can only be
called once per account. The init
constraint must be used with payer
and
space
constraints to specify the account paying for initialization and the
amount of space required.
Anchor's init_if_needed Constraint
Anchor's init_if_needed
constraint,
guarded by a feature flag, should be used with caution.It initializes an account
only if it hasn't been initialized yet. If the account is already initialized,
the instruction handler will still execute, so it's extremely important to
include checks in your instruction handler to prevent resetting the account to
its initial state.
For example, if the authority
field is set in the instruction handler, ensure
that your instruction handler includes checks to prevent an attacker from
reinitializing it after it's already been set. Typically, it's safer to have a
separate instruction handler for initializing account data.
Lab
In this lab, we'll create a simple Solana program with two instruction handlers:
insecure_initialization
- Initializes an account without checks, allowing reinitialization.recommended_initialization
- Initializes an account using Anchor'sinit
constraint, preventing reinitialization.
1. Starter
To get started, download the starter code from the
starter
branch of this repository.
The starter code includes a program with one instruction handler and the
boilerplate setup for the test file.
The insecure_initialization
instruction handler initializes a new user
account that stores the public key of an authority
. The account is expected to
be allocated client-side and then passed into the program instruction. However,
there are no checks to verify if the user
account's initial state has already
been set. This means the same account can be passed in a second time, allowing
the authority
to be overwritten.
2. Test insecure_initialization Instruction Handler
The test file includes the setup to create an account by invoking the system
program and then invokes the insecure_initialization
instruction handler twice
using the same account.
Since there are no checks in the insecure_initialization
instruction handler
to verify that the account data has not already been initialized, this
instruction handler will execute successfully both times, even with a
different authority account.
Run anchor test
to verify that the insecure_initialization
instruction
handler executes successfully in both invocations.
3. Add recommended_initialization Instruction Handler
Now, let's create a new instruction handler called recommended_initialization
that addresses the issue. Unlike the insecure instruction handler, this one will
handle both the creation and initialization of the user's account using Anchor's
init
constraint.
This constraint ensures the account is created via a CPI to the system program, and the discriminator is set. This way, any subsequent invocation with the same user account will fail, preventing reinitialization.
4. Test recommended_initialization Instruction Handler
To test the recommended_initialization
instruction handler, invoke it twice as
before. This time, the transaction should fail when attempting to initialize the
same account a second time.
Run anchor test
to confirm that the second transaction fails with an error
indicating the account is already in use.
Using Anchor's init
constraint is usually sufficient to protect against
reinitialization attacks. While the fix for these security exploits is
straightforward, it is crucial. Every time you initialize an account, ensure
that you're either using the init
constraint or implementing another check to
prevent resetting an existing account's initial state.
For the final solution code, refer to the
solution
branch of this repository.
Challenge
Your challenge is to audit your own or other programs to practice avoiding this security exploit.
Take some time to review at least one program and confirm that instruction handlers are adequately protected against reinitialization attacks.
If you find a bug or exploit in another program, alert the developer. If you find one in your own program, patch it immediately.
Push your code to GitHub and tell us what you thought of this lesson!