Data Matching
Data matching là việc bảo mật xác thực rằng dữ liệu account chứa các giá trị mong đợi trước khi tin tưởng nó trong logic chương trình của bạn. Trong khi kiểm tra owner
xác minh ai kiểm soát một account và kiểm tra signer
xác minh ủy quyền, data matching đảm bảo trạng thái bên trong của account phù hợp với các giả định của chương trình bạn.
Điều này trở nên quan trọng khi bộ xử lý instruction phụ thuộc vào mối quan hệ giữa các account hoặc khi các giá trị dữ liệu cụ thể xác định hành vi chương trình. Không có các xác minh dữ liệu thích hợp, kẻ tấn công có thể can thiệp vào luồng chương trình bằng cách tạo các account với các kết hợp dữ liệu bất ngờ, ngay cả khi những account đó vượt qua các kiểm tra ownership và authorization cơ bản.
Nguy hiểm nằm ở khoảng cách giữa việc xác minh về mặt cấu trúc và xác minh về mặt logic. Chương trình của bạn có thể xác minh chính xác rằng một account có đúng kiểu và được sở hữu bởi chương trình đúng, nhưng vẫn đưa ra các giả định không chính xác về mối quan hệ giữa các phần dữ liệu khác nhau.
Anchor
Xem xét instruction dễ bị tấn công này cập nhật ownership của một program account:
#[program]
pub mod insecure_update{
use super::*;
//..
pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
ctx.accounts.program_account.owner = ctx.accounts.new_owner.key();
Ok(())
}
//..
}
#[derive(Accounts)]
pub struct UpdateOwnership<'info> {
pub owner: Signer<'info>,
/// CHECK: This account will not be checked by Anchor
pub new_owner: UncheckedAccount<'info>,
#[account(mut)]
pub program_account: Account<'info, ProgramAccount>,
}
#[account]
pub struct ProgramAccount {
owner: Pubkey,
}
Mã này thoạt nhìn có vẻ an toàn. owner
được đánh dấu đúng cách là Signer
, đảm bảo họ đã ủy quyền cho giao dịch. program_account
được gõ chính xác và được sở hữu bởi chương trình. Tất cả các kiểm tra bảo mật cơ bản đều vượt qua.
Nhưng có một lỗ hổng nghiêm trọng: chương trình không bao giờ xác thực rằng owner
đã ký giao dịch thực sự giống với owner
được lưu trữ trong dữ liệu program_account
.
Kẻ tấn công có thể khai thác điều này bằng cách:
- Tạo keypair riêng của họ (gọi là
attacker_keypair
) - Tìm bất kỳ program account nào họ muốn chiếm đoạt
- Tạo một giao dịch nơi:
owner
làattacker_keypair
(mà họ kiểm soát và có thể ký với);new_owner
là public key chính của họ vàprogram_account
là account của nạn nhân
Giao dịch thành công vì attacker_keypair
ký đúng cách, nhưng chương trình không bao giờ kiểm tra xem attacker_keypair
có khớp với owner
thực tế được lưu trữ trong program_account.owner
hay không. Kẻ tấn công thành công chuyển ownership của account của người khác cho chính họ.
May mắn thay Anchor
làm cho việc thực hiện kiểm tra này cực kỳ dễ dàng trực tiếp trong account struct bằng cách thêm ràng buộc has_one
như thế này:
#[derive(Accounts)]
pub struct UpdateOwnership<'info> {
pub owner: Signer<'info>,
/// CHECK: This account will not be checked by Anchor
pub new_owner: UncheckedAccount<'info>,
#[account(mut, has_one = owner)]
pub program_account: Account<'info, ProgramAccount>,
}
Hoặc chúng ta có thể quyết định thay đổi thiết kế của chương trình và làm cho program_account
thành một PDA được derive từ owner
như thế này:
#[derive(Accounts)]
pub struct UpdateOwnership<'info> {
pub owner: Signer<'info>,
/// CHECK: This account will not be checked by Anchor
pub new_owner: UncheckedAccount<'info>,
#[account(
mut,
seeds = [owner.key().as_ref()],
bump
)]
pub program_account: Account<'info, ProgramAccount>,
}
Hoặc bạn có thể chỉ cần kiểm tra dữ liệu đó trong instruction bằng cách sử dụng kiểm tra ctx.accounts.program_account.owner
như thế này:
pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
if ctx.accounts.program_account.owner != ctx.accounts.owner.key() {
return Err(ProgramError::InvalidAccountData.into());
}
ctx.accounts.program_account.owner = ctx.accounts.new_owner.key();
Ok(())
}
Bằng cách thêm kiểm tra này, bộ xử lý instruction sẽ chỉ tiếp tục nếu account có owner
đúng. Nếu owner
không đúng, giao dịch sẽ thất bại.
Pinocchio
Trong Pinocchio, vì chúng ta không có khả năng thêm kiểm tra bảo mật trực tiếp bên trong account struct, chúng ta buộc phải làm như vậy trong logic instruction.
Chúng ta có thể làm điều đó bằng cách deserialize dữ liệu của account và kiểm tra giá trị owner
:
let account_data = ctx.accounts.program_account.try_borrow_data()?;
let mut account_data_slice: &[u8] = &account_data;
let account_state = ProgramAccount::try_deserialize(&mut account_data_slice)?;
if account_state.owner != self.accounts.owner.key() {
return Err(ProgramError::InvalidAccountData.into());
}