From 389efbf38c3d28541487e6454475fb6456f82355 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Fri, 6 Sep 2024 10:01:30 +0530 Subject: [PATCH 1/2] Added fix for code snippets and content --- .../program-security/account-data-matching.md | 310 ++++++++++-------- 1 file changed, 172 insertions(+), 138 deletions(-) diff --git a/content/courses/program-security/account-data-matching.md b/content/courses/program-security/account-data-matching.md index f9963a123..35cdc113b 100644 --- a/content/courses/program-security/account-data-matching.md +++ b/content/courses/program-security/account-data-matching.md @@ -12,39 +12,43 @@ description: - 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. + used in an instruction handler. - To implement data validation checks in Rust, simply compare the data stored on an account to an expected value. + ```rust if ctx.accounts.user.key() != ctx.accounts.user_data.user { return Err(ProgramError::InvalidAccountData.into()); } ``` -- In Anchor, you can use `constraint` to checks whether the given expression - evaluates to true. Alternatively, you can use `has_one` to check that a target - account field stored on the account matches the key of an account in the - `Accounts` struct. + +- In Anchor, you can use + [`constraint`](https://www.anchor-lang.com/docs/account-constraints) to check + whether the given expression evaluates to true. Alternatively, you can use + `has_one` to check that a target account field stored on the account matches + the key of an account in the `Accounts` 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. +passed into an instruction handler. -This can be useful when accounts required by an instruction have dependencies on -values stored in other accounts or if an instruction is dependent on the data -stored in an account. +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 +### Missing data validation check -The example below includes an `update_admin` instruction that updates the -`admin` field stored on an `admin_config` account. +The example below includes an `update_admin` instruction handler that updates +the `admin` field stored on an `admin_config` account. -The instruction is missing a data validation check to verify the `admin` account -signing the transaction matches the `admin` stored on the `admin_config` +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 as the `admin` account can update the `admin_config` account. +instruction handler as the `admin` account can update the `admin_config` +account. ```rust use anchor_lang::prelude::*; @@ -67,7 +71,8 @@ pub struct UpdateAdmin<'info> { pub admin_config: Account<'info, AdminConfig>, #[account(mut)] pub admin: Signer<'info>, - pub new_admin: SystemAccount<'info>, + /// CHECK: This account will not be checked by anchor + pub new_admin: UncheckedAccount<'info>, } #[account] @@ -76,11 +81,11 @@ pub struct AdminConfig { } ``` -#### Add data validation check +### 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. +error if they don't match. ```rust if ctx.accounts.admin.key() != ctx.accounts.admin_config.admin { @@ -88,9 +93,9 @@ if ctx.accounts.admin.key() != ctx.accounts.admin_config.admin { } ``` -By adding a data validation check, the `update_admin` instruction would only -process if the `admin` signer of the transaction matched the `admin` stored on -the `admin_config` account. +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. ```rust use anchor_lang::prelude::*; @@ -116,7 +121,8 @@ pub struct UpdateAdmin<'info> { pub admin_config: Account<'info, AdminConfig>, #[account(mut)] pub admin: Signer<'info>, - pub new_admin: SystemAccount<'info>, + /// CHECK: This account will not be checked by anchor + pub new_admin: UncheckedAccount<'info>, } #[account] @@ -125,11 +131,11 @@ pub struct AdminConfig { } ``` -#### Use Anchor constraints +### 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 logic to the -`UpdateAdmin` struct. +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 @@ -161,7 +167,8 @@ pub struct UpdateAdmin<'info> { pub admin_config: Account<'info, AdminConfig>, #[account(mut)] pub admin: Signer<'info>, - pub new_admin: SystemAccount<'info>, + /// CHECK: This account will not be checked by anchor + pub new_admin: UncheckedAccount<'info>, } #[account] @@ -172,7 +179,7 @@ pub struct AdminConfig { 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 +some reason naming can't be consistent or when you need a more complex expression to fully validate the incoming data. ```rust @@ -185,46 +192,51 @@ pub struct UpdateAdmin<'info> { pub admin_config: Account<'info, AdminConfig>, #[account(mut)] pub admin: Signer<'info>, - pub new_admin: SystemAccount<'info>, + /// CHECK: This account will not be checked by anchor + pub new_admin: UncheckedAccount<'info>, } ``` ## Lab -For this lab we’ll create a simple “vault” program similar to the program we +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 +those labs, we'll show in this lab how a missing data validation check could allow the vault to be drained. -#### 1. Starter +### 1. Starter -To get started, download the starter code from the `starter` branch of -[this repository](https://github.com/Unboxed-Software/solana-account-data-matching). +To get started, download the starter code from the +[`starter` branch of this repository](https://github.com/solana-developers/account-data-matching/tree/starter). The starter code includes a program with two instructions and the boilerplate setup for the test file. -The `initialize_vault` instruction 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 `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 transfers all the tokens in the `vault` -account’s token account to a `withdraw_destination` 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 \***\*does\*\*** have a signer check for + + +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 logic is there code that checks that the `authority` -account passed into the instruction matches the `authority` account on the -`vault`. +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`. ```rust use anchor_lang::prelude::*; use anchor_spl::token::{self, Mint, Token, TokenAccount}; -declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +declare_id!("J89xWAprDsLAAwcTA6AhrK49UMSAYJJWdXvw4ZQK4suu"); + +pub const DISCRIMINATOR_SIZE: usize = 8; #[program] pub mod account_data_matching { @@ -240,7 +252,7 @@ pub mod account_data_matching { pub fn insecure_withdraw(ctx: Context) -> Result<()> { let amount = ctx.accounts.token_account.amount; - let seeds = &[b"vault".as_ref(), &[*ctx.bumps.get("vault").unwrap()]]; + let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]]; let signer = [&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( @@ -263,7 +275,7 @@ pub struct InitializeVault<'info> { #[account( init, payer = authority, - space = 8 + 32 + 32 + 32, + space = DISCRIMINATOR_SIZE + Vault::INIT_SPACE, seeds = [b"vault"], bump, )] @@ -306,6 +318,7 @@ pub struct InsecureWithdraw<'info> { } #[account] +#[derive(Default, InitSpace)] pub struct Vault { token_account: Pubkey, authority: Pubkey, @@ -313,64 +326,73 @@ pub struct Vault { } ``` -#### 2. Test `insecure_withdraw` instruction +### 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. +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 -using the provider wallet as the `authority` and then mints 100 tokens to the -`vault` token account. +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. Use -`withdrawDestinationFake` as the `withdrawDestination` account and `walletFake` -as the `authority`. Then send the transaction using `walletFake`. +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 matches the values stored on the `vault` account initialized in the -first test, the instruction will process successfully and the tokens will be -transferred to the `withdrawDestinationFake` account. +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. ```typescript -describe("account-data-matching", () => { +describe("Account Data Matching", () => { ... - it("Insecure withdraw", async () => { - const tx = await program.methods - .insecureWithdraw() - .accounts({ - vault: vaultPDA, - tokenAccount: tokenPDA, - withdrawDestination: withdrawDestinationFake, - authority: walletFake.publicKey, - }) - .transaction() - - await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]) - - const balance = await connection.getTokenAccountBalance(tokenPDA) - expect(balance.value.uiAmount).to.eq(0) - }) + it("allows insecure withdrawal", async () => { + try { + const tx = await program.methods + .insecureWithdraw() + .accounts({ + vault: vaultPDA, + tokenAccount: tokenPDA, + withdrawDestination: fakeWithdrawDestination, + authority: fakeWallet.publicKey, + }) + .transaction(); + + await anchor.web3.sendAndConfirmTransaction(provider.connection, tx, [ + fakeWallet, + ]); + + const tokenAccount = await getAccount(provider.connection, tokenPDA); + expect(Number(tokenAccount.amount)).to.equal(0); + } catch (error) { + throw new Error( + `Insecure withdraw failed unexpectedly: ${error.message}`, + ); + } + }); }) ``` Run `anchor test` to see that both transactions will complete successfully. ```bash -account-data-matching - ✔ Initialize Vault (811ms) - ✔ Insecure withdraw (403ms) +Account Data Matching + ✔ initializes the vault and mints tokens (879ms) + ✔ allows insecure withdrawal (431ms) ``` -#### 3. Add `secure_withdraw` instruction +### 3. Add secure_withdraw Instruction Handler -Let’s go implement a secure version of this instruction called +Let's go implement a secure version of this instruction handler called `secure_withdraw`. -This instruction will be identical to the `insecure_withdraw` instruction, -except we’ll use the `has_one` constraint in the account validation struct -(`SecureWithdraw`) to check that the `authority` account passed into the -instruction matches the `authority` account on the `vault` account. That way -only the correct authority account can withdraw the vault’s tokens. +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. ```rust use anchor_lang::prelude::*; @@ -378,6 +400,8 @@ use anchor_spl::token::{self, Mint, Token, TokenAccount}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +pub const DISCRIMINATOR_SIZE: usize = 8; + #[program] pub mod account_data_matching { use super::*; @@ -385,7 +409,7 @@ pub mod account_data_matching { pub fn secure_withdraw(ctx: Context) -> Result<()> { let amount = ctx.accounts.token_account.amount; - let seeds = &[b"vault".as_ref(), &[*ctx.bumps.get("vault").unwrap()]]; + let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]]; let signer = [&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( @@ -411,7 +435,6 @@ pub struct SecureWithdraw<'info> { has_one = token_account, has_one = authority, has_one = withdraw_destination, - )] pub vault: Account<'info, Vault>, #[account( @@ -427,94 +450,104 @@ pub struct SecureWithdraw<'info> { } ``` -#### 4. Test `secure_withdraw` instruction +### 4. Test secure_withdraw Instruction Handler -Now let’s test the `secure_withdraw` instruction with two tests: one that uses -`walletFake` 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. +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. ```typescript describe("account-data-matching", () => { ... - it("Secure withdraw, expect error", async () => { + it("prevents unauthorized secure withdrawal", async () => { try { const tx = await program.methods .secureWithdraw() .accounts({ vault: vaultPDA, tokenAccount: tokenPDA, - withdrawDestination: withdrawDestinationFake, - authority: walletFake.publicKey, + withdrawDestination: fakeWithdrawDestination, + authority: fakeWallet.publicKey, }) - .transaction() + .transaction(); - await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]) - } catch (err) { - expect(err) - console.log(err) + await anchor.web3.sendAndConfirmTransaction(provider.connection, tx, [ + fakeWallet, + ]); + + throw new Error("Secure withdraw should have failed but didn't"); + } catch (error) { + expect(error).to.be.an("error"); + console.log("Expected error occurred:", error.message); } - }) - - it("Secure withdraw", async () => { - await spl.mintTo( - connection, - wallet.payer, - mint, - tokenPDA, - wallet.payer, - 100 - ) - - await program.methods - .secureWithdraw() - .accounts({ - vault: vaultPDA, - tokenAccount: tokenPDA, - withdrawDestination: withdrawDestination, - authority: wallet.publicKey, - }) - .rpc() - - const balance = await connection.getTokenAccountBalance(tokenPDA) - expect(balance.value.uiAmount).to.eq(0) - }) + }); + + it("allows secure withdrawal by authorized user", async () => { + try { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await mintTo( + provider.connection, + wallet.payer, + mint, + tokenPDA, + wallet.payer, + 100, + ); + + await program.methods + .secureWithdraw() + .accounts({ + vault: vaultPDA, + tokenAccount: tokenPDA, + withdrawDestination, + authority: wallet.publicKey, + }) + .rpc(); + + const tokenAccount = await getAccount(provider.connection, tokenPDA); + expect(Number(tokenAccount.amount)).to.equal(0); + } catch (error) { + throw new Error(`Secure withdraw failed unexpectedly: ${error.message}`); + } + }); }) ``` Run `anchor test` to see that the transaction using an incorrect authority -account will now return an Anchor Error while the transaction using correct -accounts completes successfully. +account will now return an Anchor Error while the transaction using the correct +accounts complete successfully. ```bash -'Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]', -'Program log: Instruction: SecureWithdraw', -'Program log: AnchorError caused by account: vault. Error Code: ConstraintHasOne. Error Number: 2001. Error Message: A has one constraint was violated.', -'Program log: Left:', -'Program log: DfLZV18rD7wCQwjYvhTFwuvLh49WSbXFeJFPQb5czifH', -'Program log: Right:', -'Program log: 5ovvmG5ntwUC7uhNWfirjBHbZD96fwuXDMGXiyMwPg87', -'Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 10401 of 200000 compute units', -'Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS failed: custom program error: 0x7d1' +"Program J89xWAprDsLAAwcTA6AhrK49UMSAYJJWdXvw4ZQK4suu invoke [1]", +"Program log: Instruction: SecureWithdraw", +"Program log: AnchorError caused by account: vault. Error Code: ConstraintHasOne. Error Number: 2001. Error Message: A has one constraint was violated.", +"Program log: Left:", +"Program log: GprrWv9r8BMxQiWea9MrbCyK7ig7Mj8CcseEbJhDDZXM", +"Program log: Right:", +"Program log: 2jTDDwaPzbpG2oFnnqtuHJpiS9k9dDVqzzfA2ofcqfFS", +"Program J89xWAprDsLAAwcTA6AhrK49UMSAYJJWdXvw4ZQK4suu consumed 11790 of 200000 compute units", +"Program J89xWAprDsLAAwcTA6AhrK49UMSAYJJWdXvw4ZQK4suu failed: custom program error: 0x7d1" ``` Note that Anchor specifies in the logs the account that causes the error (`AnchorError caused by account: vault`). ```bash -✔ Secure withdraw, expect error (77ms) -✔ Secure withdraw (10073ms) +✔ prevents unauthorized secure withdrawal +✔ allows secure withdrawal by authorized user (1713ms) ``` 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 becomse increasingly easy to miss +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](https://github.com/Unboxed-Software/solana-account-data-matching/tree/solution). +[`solution` branch of the repository](https://github.com/solana-developers/account-data-matching/tree/solution). ## Challenge @@ -528,6 +561,7 @@ 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](https://form.typeform.com/to/IPH0UGz7#answers-lesson=a107787e-ad33-42bb-96b3-0592efc1b92f)! From ddfd44bee6efbc8dafab9de34b37f434ae38db5b Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Fri, 4 Oct 2024 17:03:13 +1000 Subject: [PATCH 2/2] Update content/courses/program-security/account-data-matching.md --- content/courses/program-security/account-data-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/courses/program-security/account-data-matching.md b/content/courses/program-security/account-data-matching.md index 35cdc113b..6ecb10f5a 100644 --- a/content/courses/program-security/account-data-matching.md +++ b/content/courses/program-security/account-data-matching.md @@ -22,7 +22,7 @@ description: } ``` -- In Anchor, you can use +- In Anchor, you can use a [`constraint`](https://www.anchor-lang.com/docs/account-constraints) to check whether the given expression evaluates to true. Alternatively, you can use `has_one` to check that a target account field stored on the account matches