Summary
-
Use data validation checks to verify that account data matches an expected value. Without appropriate data validation checks, unexpected accounts may be used in an instruction handler.
-
To implement data validation checks in Rust, simply compare the data stored on an account to an expected value.
-
In Anchor, you can use a
constraint
to check whether the given expression evaluates to true. Alternatively, you can usehas_one
to check that a target account field stored on the account matches the key of an account in theAccounts
struct.
Lesson
Account data matching refers to data validation checks used to verify the data stored on an account matches an expected value. Data validation checks provide a way to include additional constraints to ensure the appropriate accounts are passed into an instruction handler.
This can be useful when accounts required by an instruction handler have dependencies on values stored in other accounts or if an instruction handler is dependent on the data stored in an account.
Missing data validation check
The example below includes an update_admin
instruction handler that updates
the admin
field stored on an admin_config
account.
The instruction handler is missing a data validation check to verify the admin
account signing the transaction matches the admin
stored on the admin_config
account. This means any account signing the transaction and passed into the
instruction handler as the admin
account can update the admin_config
account.
Add Data Validation Check
The basic Rust approach to solve this problem is to simply compare the passed in
admin
key to the admin
key stored in the admin_config
account, throwing an
error if they don't match.
By adding a data validation check, the update_admin
instruction handler would
only process if the admin
signer of the transaction matched the admin
stored
on the admin_config
account.
Use Anchor Constraints
Anchor simplifies this with the has_one
constraint. You can use the has_one
constraint to move the data validation check from the instruction handler logic
to the UpdateAdmin
struct.
In the example below, has_one = admin
specifies that the admin
account
signing the transaction must match the admin
field stored on the
admin_config
account. To use the has_one
constraint, the naming convention
of the data field on the account must be consistent with the naming on the
account validation struct.
Alternatively, you can use constraint
to manually add an expression that must
evaluate to true in order for execution to continue. This is useful when for
some reason naming can't be consistent or when you need a more complex
expression to fully validate the incoming data.
Lab
For this lab, we'll create a simple “vault” program similar to the program we used in the Signer Authorization lesson and the Owner Check lesson. Similar to those labs, we'll show in this lab how a missing data validation check could allow the vault to be drained.
1. Starter
To get started, download the starter code from the
starter
branch of this repository.
The starter code includes a program with two instructions and the boilerplate
setup for the test file.
The initialize_vault
instruction handler initializes a new Vault
account and
a new TokenAccount
. The Vault
account will store the address of a token
account, the authority of the vault, and a withdraw destination token account.
The authority of the new token account will be set as the vault
, a PDA of the
program. This allows the vault
account to sign for the transfer of tokens from
the token account.
The insecure_withdraw
instruction handler transfers all the tokens in the
vault
account's token account to a withdraw_destination
token account.
Notice that this instruction handler does have a signer check for
authority
and an owner check for vault
. However, nowhere in the account
validation or instruction handler logic is there code that checks that the
authority
account passed into the instruction handler matches the authority
account on the vault
.
2. Test insecure_withdraw Instruction Handler
To prove that this is a problem, let's write a test where an account other than
the vault's authority
tries to withdraw from the vault.
The test file includes the code to invoke the initialize_vault
instruction
handler using the provider wallet as the authority
and then mints 100 tokens
to the vault
token account.
Add a test to invoke the insecure_withdraw
instruction handler. Use
fakeWithdrawDestination
as the withdrawDestination
account and fakeWallet
as the authority
. Then send the transaction using fakeWallet
.
Since there are no checks the verify the authority
account passed into the
instruction handler matches the values stored on the vault
account initialized
in the first test, the instruction handler will process successfully and the
tokens will be transferred to the fakeWithdrawDestination
account.
Run anchor test
to see that both transactions will complete successfully.
3. Add secure_withdraw Instruction Handler
Let's go implement a secure version of this instruction handler called
secure_withdraw
.
This instruction handler will be identical to the insecure_withdraw
instruction handler, except we'll use the has_one
constraint in the account
validation struct (SecureWithdraw
) to check that the authority
account
passed into the instruction handler matches the authority
account on the
vault
account. That way only the correct authority account can withdraw the
vault's tokens.
4. Test secure_withdraw Instruction Handler
Now let's test the secure_withdraw
instruction handler with two tests: one
that uses fakeWallet
as the authority and one that uses wallet
as the
authority. We expect the first invocation to return an error and the second to
succeed.
Run anchor test
to see that the transaction using an incorrect authority
account will now return an Anchor Error while the transaction using the correct
accounts complete successfully.
Note that Anchor specifies in the logs the account that causes the error
(AnchorError caused by account: vault
).
And just like that, you've closed up the security loophole. The theme across most of these potential exploits is that they're quite simple. However, as your programs grow in scope and complexity, it becomes increasingly easy to miss possible exploits. It's great to get in a habit of writing tests that send instructions that shouldn't work. The more the better. That way you catch problems before you deploy.
If you want to take a look at the final solution code you can find it on the
solution
branch of the repository.
Challenge
Just as with other lessons in this unit, your opportunity to practice avoiding this security exploit lies in auditing your own or other programs.
Take some time to review at least one program and ensure that proper data checks are in place to avoid security exploits.
Remember, if you find a bug or exploit in somebody else's program, please alert them! If you find one in your own program, be sure to patch it right away.
Push your code to GitHub and tell us what you thought of this lesson!