Summary
- Owner checks ensure that accounts are owned by the expected program. Without owner checks, accounts owned by other programs can be used in an instruction handler.
- Anchor program account types implement the
Owner
trait, allowingAccount<'info, T>
to automatically verify program ownership. - You can also use Anchor's
#[account(owner = <expr>)]
constraint to define an account's owner when it's external to the current program. - To implement an owner check in native Rust, verify that the account's owner matches the expected program ID.
Lesson
Owner checks are used to verify that an account passed into an instruction handler is owned by the expected program, preventing exploitation by accounts from different programs.
The AccountInfo
struct contains several fields, including the owner
, which
represents the program that owns the account. Owner checks ensure that this
owner
field in the AccountInfo
matches the expected program ID.
Missing owner check
In the following example, an admin_instruction
is intended to be restricted to
an admin
account stored in the admin_config
account. However, it fails to
check whether the program owns the admin_config
account. Without this check,
an attacker can spoof the account.
Add owner check
To resolve this issue in native Rust, compare the owner
field with the program
ID:
Adding an owner
check ensures that accounts from other programs cannot be
passed into the instruction handler.
Use Anchor's Account<'info, T>
Anchor simplifies owner checks with the Account
type, which wraps
AccountInfo
and automatically verifies ownership.
In the following example, Account<'info, AdminConfig>
validates the
admin_config
account, and the has_one
constraint checks that the admin
account matches the admin
field in admin_config
.
Use Anchor's #[account(owner = <expr>)]
constraint
In addition to the Account
type, you can use the Anchor's
owner
constraint to
specify the program that should own an account when it differs from the
executing program. This is particularly useful when an instruction handler
expects an account to be a PDA created by another program. By using the seeds
and bump
constraints along with the owner
, you can properly derive and
verify the account's address.
To apply the owner
constraint, you need access to the public key of the
program expected to own the account. This can be provided either as an
additional account or by hard-coding the public key within your program.
Lab
In this lab, we'll demonstrate how the absence of an owner check can allow a malicious actor to drain tokens from a simplified token vault. This is similar to the lab from the Signer Authorization lesson.
We'll use two programs to illustrate this:
- One program lacks an owner check on the vault account it withdraws tokens from.
- The second program is a clone created by a malicious user to mimic the first program's vault account.
Without the owner check, the malicious user can pass in their vault account owned by a fake program, and the original program will still execute the withdrawal.
1. Starter
Begin by downloading the starter code from the
starter
branch of this repository.
The starter code includes two programs: clone
and owner_check
, and the setup
for the test file.
The owner_check
program includes two instruction handlers:
initialize_vault
: Initializes a simplified vault account storing the addresses of a token account and an authority account.insecure_withdraw
: Withdraws tokens from the token account but lacks an owner check for the vault account.
The clone
program includes a single instruction handler:
initialize_vault
: Initializes a fake vault account that mimics the vault account of theowner_check
program, allowing the malicious user to set their own authority.
2. Test insecure_withdraw Instruction Handler
The test file contains tests that initialize a vault in both programs. We'll add
a test to invoke the insecure_withdraw
instruction handler, showing how the
lack of an owner check allows token withdrawal from the original program's
vault.
Run an anchor test
to verify that the insecure_withdraw
is complete
successfully.
The vaultCloneAccount
deserializes successfully due to both programs using the
same discriminator, derived from the identical Vault
struct name.
3. Add secure_withdraw Instruction Handler
We'll now close the security loophole by adding a secure_withdraw
instruction
handler with an Account<'info, Vault>
type to ensure an owner check is
performed.
In the lib.rs
file of the owner_check
program, add a secure_withdraw
instruction handler and a SecureWithdraw
accounts struct. The has_one
constraint will be used to ensure that the token_account
and authority
passed into the instruction handler match the values stored in the vault
account.
4. Test secure_withdraw Instruction Handler
To test the secure_withdraw
instruction handler, we'll invoke it twice. First,
we'll use the vaultCloneAccount
account, expecting it to fail. Then, we'll
invoke the instruction handler with the correct vaultAccount
account to verify
the instruction handler works as intended.
Running anchor test
will show that the transaction using the
vaultCloneAccount
account fails, while the transaction using the
vaultAccount
account withdraws successfully.
Here we see how using Anchor's Account<'info, T>
type simplifies the account
validation process by automating ownership checks. Additionally, Anchor errors
provide specific details, such as which account caused the error. For example,
the log indicates AnchorError caused by account: vault
, which aids in
debugging.
Ensuring account ownership checks is critical to avoid security vulnerabilities. This example demonstrates how simple it is to implement proper validation, but it's vital to always verify which accounts are owned by specific programs.
If you'd like to review the final solution code, it's available on the
solution
branch of the repository.
Challenge
As with other lessons in this unit, practice preventing security exploits by auditing your own or other programs.
Take time to review at least one program to confirm that ownership checks are properly enforced on all accounts passed into each instruction handler.
If you find a bug or exploit in another program, notify 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!