Account Data Matching exploit

By Noah 'nheindev' Hein on Sep 6, 2022
Abstract styled artwork showing similar looking data

Overview

The account data matching vulnerability is when your program does not properly validate the structure of the data you are acting upon. When you do not properly validate your program’s data shape, a bad actor can pass in arbitrary data that executes in malicious and unexpected ways. See the example below.

Insecure

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_pack::Pack;
use spl_token::state::Account as SplTokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod account_data_matching_insecure {
    use super::*;

    pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
        // token.data could be any `AccountInfo`
        let token = SplTokenAccount::unpack(&ctx.accounts.token.data.borrow())?;
        msg!("Your account balance is: {}", token.amount);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
    token: AccountInfo<'info>,
    authority: Signer<'info>,
}

This code is insecure because we do not validate the the value our LogMessage token takes in is actually a token account. All we do is check that it is of the type AccountInfo; which could be any number of different things. To patch this you need to check that the LogMessage has an authority key that matches the token.owner‘s.

Secure

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_pack::Pack;
use spl_token::state::Account as SplTokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod account_data_matching_secure {
    use super::*;

    pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
        let token = SplTokenAccount::unpack(&ctx.accounts.token.data.borrow())?;
        // check happens here
        if ctx.accounts.authority.key != &token.owner {
            return Err(ProgramError::InvalidAccountData);
        }
        msg!("Your acocunt balance is: {}", token.amount);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
    token: AccountInfo<'info>,
    authority: Signer<'info>,
}

This is now secured since we validated that the data matches our expected values. But this makes our programs a bit noisy. Anchor provides us the ability to do this validation inside our LogMessage struct, which cleans up our program’s code.

use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod account_data_matching_recommended {
    use super::*;

    pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
        msg!("Your acocunt balance is: {}", ctx.accounts.token.amount);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
    // These two lines are where the magic happens
    #[account(constraint = authority.key == &token.owner)]
    token: Account<'info, TokenAccount>,
    authority: Signer<'info>,
}

Here you can see that in the program logic we are no longer doing any validation. This makes it much easier to see what is going on in the program, without all of the noise that data validation causes. Validation has moved from the program to LogMessage.

We have imported the TokenAccount which we use in place of the generic AccountInfo. This will check that the account that gets passed into token has valid data. We also have the ability to add arbitrary validations through the raw constraint expression. In this instance we check that the authority.key == &token.owner.

Subscribe to my Newsletter

I'll be honest with you this button doesn't do anything. But I figured if I put it here it would eventually shame me into plugging this into revue or convertkit or something. If you read this and want to hear from me in newsletter form, tell me as much on Twitter!

© Copyright2023 by NHeinDev