Summary
- When an instruction requires two mutable accounts of the same type, an attacker can pass in the same account twice, causing the account to be mutated in unintended ways.
-
To check for duplicate mutable accounts in Rust, simply compare the public
keys of the two accounts and throw an error if they are the same.
-
In Anchor, you can use
constraintto add an explicit constraint to an account checking that it is not the same as another account.
Lesson
Duplicate Mutable Accounts refers to an instruction that requires two mutable accounts of the same type. When this occurs, you should validate that two accounts are different to prevent the same account from being passed into the instruction twice. Since the program treats each account as separate, passing in the same account twice could result in the second account being mutated in unintended ways. This could result in very minor issues, or catastrophic ones - it really depends on what data the code changes and how these accounts are used. Regardless, this is a vulnerability all developers should be aware of.No check
For example, imagine a program that updates adata field for user_a and
user_b in a single instruction. The value that the instruction sets for
user_a is different from user_b. Without verifying that user_a and
user_b are different, the program would update the data field on the
user_a account, then update the data field a second time with a different
value under the assumption that user_b is a separate account.
You can see this example in the code below.Tthere is no check to verify that
user_a and user_b are not the same account. Passing in the same account for
user_a and user_b will result in the data field for the account being set
to b even though the intent is to set both values a and b on separate
accounts. Depending on what data represents, this could be a minor unintended
side-effect, or it could mean a severe security risk. allowing user_a and
user_b to be the same account could result in
Add check in instruction
To fix this problem with plan Rust, simply add a check in the instruction logic to verify that the public key ofuser_a isn’t the same as the public key of
user_b, returning an error if they are the same.
user_a and user_b are not the same account.
Use Anchor constraint
An even better solution if you’re using Anchor is to add the check to the
account validation struct instead of the instruction logic.
You can use the #[account(..)] attribute macro and the constraint keyword to
add a manual constraint to an account. The constraint keyword will check
whether the expression that follows evaluates to true or false, returning an
error if the expression evaluates to false.
The example below moves the check from the instruction logic to the account
validation struct by adding a constraint to the #[account(..)] attribute.
Lab
Let’s practice by creating a simple Rock Paper Scissors program to demonstrate how failing to check for duplicate mutable accounts can cause undefined behavior within your program. This program will initialize “player” accounts and have a separate instruction that requires two player accounts to represent starting a game of rock paper scissors.- An
initializeinstruction to initialize aPlayerStateaccount - A
rock_paper_scissors_shoot_insecureinstruction that requires twoPlayerStateaccounts, but does not check that the accounts passed into the instruction are different - A
rock_paper_scissors_shoot_secureinstruction that is the same as therock_paper_scissors_shoot_insecureinstruction but adds a constraint that ensures the two player accounts are different
1. Starter
To get started, download the starter code on thestarter branch
of this repository.
The starter code includes a program with two instructions and the boilerplate
setup for the test file.
The initialize instruction initializes a new PlayerState account that stores
the public key of a player and a choice field that is set to None.
The rock_paper_scissors_shoot_insecure instruction requires two PlayerState
accounts and requires a choice from the RockPaperScissors enum for each
player, but does not check that the accounts passed into the instruction are
different. This means a single account can be used for both PlayerState
accounts in the instruction.
2. Test rock_paper_scissors_shoot_insecure instruction
The test file includes the code to invoke the initialize instruction twice to
create two player accounts.
Add a test to invoke the rock_paper_scissors_shoot_insecure instruction by
passing in the playerOne.publicKey for as both playerOne and playerTwo.
anchor test to see that the transactions completes successfully, even
though the same account is used as two accounts in the instruction. Since the
playerOne account is used as both players in the instruction, note the
choice stored on the playerOne account is also overridden and set
incorrectly as scissors.
playerOne’s choice should be rock or scissors, so
the program behavior is strange.
3. Add rock_paper_scissors_shoot_secure instruction
Next, return to lib.rs and add a rock_paper_scissors_shoot_secure
instruction that uses the #[account(...)] macro to add an additional
constraint to check that player_one and player_two are different accounts.
7. Test rock_paper_scissors_shoot_secure instruction
To test the rock_paper_scissors_shoot_secure instruction, we’ll invoke the
instruction twice. First, we’ll invoke the instruction using two different
player accounts to check that the instruction works as intended. Then, we’ll
invoke the instruction using the playerOne.publicKey as both player accounts,
which we expect to fail.
anchor test to see that the instruction works as intended and using the
playerOne account twice returns the expected error.
solution branch of
the repository.