
Panduan Penjelajah untuk Keamanan Program Solana
Daftar Isi
- Pendahuluan
- Pola Pikir Penyerang dalam Mengeksploitasi Program Solana
- Model Pemrograman Solana
- Solana Dikendalikan oleh Penyerang
- Potensi Vektor Serangan
- Strategi Mitigasi
- Pencocokan Data Akun
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Realokasi Data Akun
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Pemuatan Ulang Akun
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- CPI Arbitrer
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Fungsionalitas Transfer Otoritas
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Kanonisasi Bump Seed
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Menutup Akun
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Akun Mutable Duplikat
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Frontrunning
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Inisialisasi yang Tidak Aman
- Contoh Tidak Aman dan Cara Memitigasinya
- Kehilangan Presisi
- Kerentanan
- Perkalian Setelah Pembagian
- Fungsi Aritmetika
- Kesalahan Pembulatan
- Pemeriksaan Kepemilikan yang Tidak Ada
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Akun Read-Only
- Pemeriksaan Penanda Tangan yang Tidak Ada
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Overflow dan Underflow
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Casting
- Berbagi PDA
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Akun Tersisa
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Error Khusus Rust
- Unsafe Rust
- Panic dan Pengelolaan Error
- Tabrakan Seed
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Type Cosplay
- Kerentanan
- Contoh Skenario
- Mitigasi yang Direkomendasikan
- Kesimpulan
- Sumber Daya Tambahan
Artikel ini ditulis bersama bl0ckpain, seorang peneliti keamanan dan developer smart contract yang sebelumnya bekerja dengan Kudelski Security dan Halborn
Pendahuluan
Keamanan program Solana bukan sekadar mencegah peretas mencuri dana proyek — tetapi juga memastikan program berperilaku sebagaimana mestinya, sesuai dengan spesifikasi proyek dan ekspektasi pengguna. Keamanan program Solana dapat memengaruhi performa, skalabilitas, dan interoperabilitas dApp. Karena itu, developer harus memahami potensi vektor serangan dan kerentanan umum sebelum membangun aplikasi untuk konsumen.
Artikel ini membahas kerentanan umum yang akan dihadapi developer saat membuat program Solana. Kami memulai dengan pengantar mengenai pola pikir penyerang dalam mengeksploitasi program Solana, yang mencakup topik seperti model pemrograman Solana, bagaimana desain Solana pada dasarnya dikendalikan oleh penyerang, potensi vektor serangan, dan strategi mitigasi umum. Selanjutnya, kami membahas berbagai kerentanan, disertai penjelasan serta contoh kode yang tidak aman dan aman jika relevan.
Perhatikan bahwa artikel ini ditujukan bagi pembaca tingkat menengah atau lanjutan karena mengasumsikan pemahaman tentang model pemrograman dan pengembangan program Solana.
Artikel ini tidak akan membahas proses membangun program atau konsep khusus Solana — kami berfokus pada pengkajian kerentanan umum dan cara memitigasinya. Jika Anda baru mengenal Solana, sebaiknya baca artikel blog berikut sebelum melanjutkan artikel ini:
Pola Pikir Penyerang dalam Mengeksploitasi Program Solana
Model Pemrograman Solana
Model pemrograman Solana membentuk lanskap keamanan aplikasi yang dibangun di jaringannya. Di Solana, akun berfungsi sebagai wadah data, serupa dengan file di komputer. Akun dapat dibagi menjadi dua jenis umum: dapat dieksekusi dan tidak dapat dieksekusi. Akun yang dapat dieksekusi, atau program, adalah akun yang mampu menjalankan kode. Akun yang tidak dapat dieksekusi digunakan untuk menyimpan data tanpa kemampuan menjalankan kode (karena tidak menyimpan kode apa pun). Pemisahan kode dan data ini berarti program tidak memiliki status — program berinteraksi dengan data yang tersimpan di akun lain dan diteruskan sebagai referensi selama transaksi.
Solana Dikendalikan oleh Penyerang
Sebuah transaksi menentukan program yang akan dipanggil, daftar akun, dan array byte berisi data instruksi. Model ini mengandalkan program untuk mengurai dan menafsirkan akun serta instruksi yang diberikan oleh transaksi tertentu. Kemampuan untuk meneruskan akun apa pun ke fungsi program memberi penyerang kendali besar atas data yang akan diproses program. Memahami bahwa model pemrograman Solana pada dasarnya dikendalikan oleh penyerang sangat penting untuk mengembangkan program yang aman.
Karena penyerang dapat meneruskan akun apa pun ke fungsi program, validasi data menjadi pilar utama keamanan program Solana. Developer harus memastikan program mereka dapat membedakan input yang sah dari input berbahaya. Ini mencakup verifikasi kepemilikan akun, memastikan akun memiliki jenis yang diharapkan, dan memeriksa apakah suatu akun merupakan penanda tangan.
Potensi Vektor Serangan
Model pemrograman dan lingkungan eksekusi Solana yang unik menimbulkan vektor serangan tertentu. Memahami vektor ini sangat penting agar developer dapat melindungi program mereka dari potensi eksploitasi. Vektor serangan tersebut meliputi:
- Bug Logika: kelemahan dalam logika program dapat dimanipulasi untuk menyebabkan perilaku yang tidak diinginkan, seperti kehilangan aset atau akses tanpa izin. Ini juga mencakup kegagalan menerapkan spesifikasi proyek dengan benar — jika program mengklaim melakukan x, program tersebut harus melakukan x beserta seluruh karakteristik khususnya
- Kelemahan Validasi Data: validasi data input yang tidak memadai dapat memungkinkan penyerang memasukkan data berbahaya dan memanipulasi status atau eksekusi program
- Masalah Khusus Rust: terlepas dari fitur keamanan Rust, blok kode yang tidak aman, masalah konkurensi, dan panic dapat menimbulkan kerentanan
- Kerentanan Kontrol Akses: kegagalan menerapkan pemeriksaan kontrol akses dengan benar, seperti memverifikasi pemilik akun, dapat menyebabkan tindakan tanpa izin oleh pelaku berbahaya
- Kesalahan Aritmetika dan Presisi: overflow/underflow dan kesalahan presisi dapat dieksploitasi untuk memperoleh keuntungan finansial atau menyebabkan program tidak berfungsi
- Masalah Cross-Program Invocation (CPI): kelemahan dalam penanganan CPI dapat mengakibatkan perubahan status atau kesalahan yang tidak terduga jika program yang dipanggil berperilaku jahat atau tidak sesuai harapan
- Penyalahgunaan Program Derived Addresses (PDA): pembuatan atau penanganan PDA yang tidak tepat dapat menimbulkan kerentanan yang memungkinkan penyerang mengambil alih atau memalsukan PDA untuk memperoleh akses tanpa izin atau memanipulasi akun yang dikendalikan program
Perhatikan bahwa reentrancy pada dasarnya terbatas di Solana karena model eksekusinya. Runtime Solana membatasi CPI hingga kedalaman maksimum empat dan menerapkan aturan akun yang ketat, seperti hanya mengizinkan pemilik akun untuk mengubah datanya. Batasan ini mencegah serangan reentrancy dengan membatasi rekursi mandiri langsung dan memastikan program tidak dapat dipanggil secara paksa dalam status perantara.
Strategi Mitigasi
Untuk memitigasi potensi serangan ini, developer sebaiknya menggabungkan pengujian ketat, audit kode, dan kepatuhan terhadap praktik terbaik:
- Terapkan validasi input dan pemeriksaan kontrol akses yang komprehensif
- Manfaatkan sistem tipe dan fitur keamanan Rust semaksimal mungkin, serta hindari kode yang tidak aman kecuali diperlukan
- Ikuti praktik terbaik keamanan Solana dan Rust serta terus ikuti perkembangan terbaru
- Lakukan peninjauan kode internal dan gunakan alat otomatis untuk mengidentifikasi kerentanan umum serta kesalahan logika selama pengembangan program
- Audit basis kode Anda melalui pihak ketiga tepercaya, termasuk perusahaan keamanan dan peneliti keamanan independen
- Buat platform bug bounty untuk program Anda guna mendorong pelaporan kerentanan daripada mengandalkan peretas topi abu-abu
Bagian berikut akan membahas berbagai kerentanan menurut urutan alfabet. Setiap bagian akan menjelaskan potensi kerentanan, cara memitigasinya, dan memberikan contoh skenario jika memungkinkan.
Pencocokan Data Akun
Kerentanan
Pencocokan data akun adalah kerentanan yang muncul ketika developer tidak memeriksa apakah data yang tersimpan di suatu akun sesuai dengan serangkaian nilai yang diharapkan. Tanpa pemeriksaan validasi data yang tepat, program dapat secara tidak sengaja beroperasi dengan akun yang salah atau diganti secara berbahaya. Kerentanan ini sangat serius dalam skenario yang melibatkan pemeriksaan terkait izin.
Contoh Skenario
Pertimbangkan sebuah program dengan fungsionalitas untuk mengelola pengaturan administratifnya. Program tersebut menyertakan instruksi untuk memperbarui konfigurasi administratif saat ini, seperti flag fitur atau parameter operasional. Instruksi harus memvalidasi bahwa permintaan berasal dari administrator yang berwenang. Namun, program gagal memverifikasi bahwa akun yang meminta perubahan cocok dengan akun administrator yang tersimpan dalam data konfigurasi:
pub fn update_admin_settings(ctx: Context<UpdateAdminSettings>, new_settings: AdminSettings) -> Result<()> {
ctx.accounts.config_data.settings = new_settings;
Ok(())
}
#[derive(Accounts)]
pub struct UpdateAdminSettings<'info> {
#[account(mut)]
pub config_data: Account<'info, ConfigData>,
pub admin: Signer<'info>,
}
#[account]
pub struct ConfigData {
admin: Pubkey,
settings: AdminSettings
}Mitigasi yang Direkomendasikan
Untuk memitigasi kerentanan ini, developer dapat menerapkan pemeriksaan eksplisit yang membandingkan kunci akun dan data tersimpan dengan nilai yang diharapkan. Misalnya, verifikasi bahwa kunci publik penyetor cocok dengan kolom pemilik akun token yang digunakan untuk deposit:
pub fn update_admin_settings(ctx: Context<UpdateAdminSettings>, new_settings: AdminSettings) -> Result<()> {
if ctx.accounts.admin.key() != ctx.accounts.config_data.admin {
return Err(ProgramError::Unauthorized);
}
ctx.accounts.config_data.settings = new_settings;
Ok(())
}Developer juga dapat menggunakan atribut has_one dan constraint milik Anchor untuk menerapkan pemeriksaan validasi data secara deklaratif. Menggunakan contoh di atas, kita dapat memakai atribut constraint untuk memeriksa bahwa kunci publik penyetor dan pemilik akun token deposit adalah sama:
pub struct UpdateAdminSettings<'info> {
#[account(
mut,
constraint = config_data.admin == admin.key()
)]
pub config_data: Account<'info, ConfigData>,
pub admin: Signer<'info>,
}Realokasi Data Akun
Kerentanan
Di Anchor, fungsi realloc yang disediakan oleh struct AccountInfo menimbulkan kerentanan bernuansa terkait pengelolaan memori. Fungsi ini memungkinkan realokasi ukuran data akun, yang dapat berguna untuk menangani data dinamis dalam program. Namun, penggunaan realloc yang tidak tepat dapat menimbulkan konsekuensi yang tidak diinginkan, termasuk membuang-buang unit komputasi atau berpotensi mengekspos data lama.
Metode realloc memiliki dua parameter:
- new_len: usize yang menentukan panjang baru data akun
- zero_init: bool yang menentukan apakah ruang memori baru harus diinisialisasi dengan nol
realloc didefinisikan sebagai berikut:
pub fn realloc(
&self,
new_len: usize,
zero_init: bool
) -> Result<(), ProgramError>Memori yang dialokasikan untuk data akun sudah diinisialisasi dengan nol pada titik masuk program. Artinya, ruang memori baru sudah berisi nol saat data direalokasi ke ukuran yang lebih besar dalam satu transaksi. Mengisi ulang memori ini dengan nol tidak diperlukan dan mengakibatkan konsumsi unit komputasi tambahan. Sebaliknya, realokasi ke ukuran yang lebih kecil lalu kembali ke ukuran yang lebih besar dalam transaksi yang sama dapat mengekspos data lama jika zero_init bernilai false.
Contoh Skenario
Pertimbangkan program daftar tugas dinamis yang memungkinkan pengguna menambah, menghapus, atau mengubah entri dalam satu transaksi. Program ini perlu merealokasi ukuran datanya secara dinamis berdasarkan tindakan pengguna:
pub fn modify_todo_list(ctx: Context<ModifyTodoList>, modifications: Vec<TodoModification>) -> ProgramResult {
// Logic to process modifications
for modification in modifications {
match modification {
TodoModification::Add(entry) => {
// Add logic
},
TodoModification::Remove(index) => {
// Remove logic, potentially requiring data reallocation
},
TodoModification::Edit(index, new_entry) => {
// Edit logic
},
}
}
// Reallocation logic to adjust the data size based on modifications
let required_data_len = calculate_required_data_len(&modifications);
ctx.accounts.todo_list_data.realloc(required_data_len, false)?;
Ok(())
}
#[derive(Accounts)]
pub struct ModifyTodoList<'info> {
#[account(mut)]
todo_list_data: AccountInfo<'info>,
// Other relevant accounts
}Dalam skenario ini, fungsi modify_todo_list dapat merealokasi to_do_list_data beberapa kali untuk mengakomodasi ukuran yang diperlukan oleh perubahan. Jika ukuran data dikurangi untuk menghapus entri tugas lalu ditingkatkan kembali guna menambahkan entri baru dalam transaksi yang sama, menetapkan zero_init ke false dapat mengekspos data lama.
Mitigasi yang Direkomendasikan
Untuk memitigasi masalah ini, parameter zero_init harus digunakan secara bijak:
- Tetapkan
zero_initketruesaat meningkatkan ukuran data setelah sebelumnya dikurangi dalam pemanggilan transaksi yang sama. Ini memastikan setiap ruang memori baru diinisialisasi dengan nol sehingga data lama tidak terekspos - Tetapkan
zero_initkefalsesaat meningkatkan ukuran data tanpa pengurangan sebelumnya dalam pemanggilan transaksi yang sama karena memori sudah diinisialisasi dengan nol
Alih-alih merealokasi data untuk memenuhi persyaratan ukuran tertentu, developer sebaiknya menggunakan Address Lookup Tables (ALT). ALT memungkinkan developer mengompresi data transaksi dengan menyimpan hingga 256 alamat dalam satu akun on-chain. Setiap alamat dalam tabel kemudian dapat dirujuk melalui indeks 1 byte, yang secara signifikan mengurangi data yang diperlukan untuk referensi alamat dalam transaksi tertentu. ALT jauh lebih berguna untuk skenario yang memerlukan interaksi akun dinamis tanpa perlu sering mengubah ukuran memori.
Pemuatan Ulang Akun
Kerentanan
Pemuatan ulang akun adalah kerentanan yang muncul ketika developer gagal memperbarui akun yang telah dideserialisasi setelah melakukan CPI. Anchor tidak secara otomatis menyegarkan status akun yang telah dideserialisasi setelah CPI. Hal ini dapat menyebabkan skenario ketika logika program beroperasi pada data lama sehingga menimbulkan kesalahan logika atau perhitungan yang keliru.
Contoh Skenario
Pertimbangkan protokol yang memungkinkan pengguna melakukan staking token untuk memperoleh imbalan seiring waktu. Program yang memfasilitasi hal ini menyertakan fungsionalitas untuk memperbarui imbalan staking pengguna berdasarkan kondisi tertentu atau pemicu eksternal. Imbalan pengguna dihitung dan diperbarui melalui CPI ke program distribusi imbalan. Namun, program gagal memperbarui akun staking asli setelah CPI untuk mencerminkan saldo imbalan baru:
pub fn update_rewards(ctx: Context<UpdateStakingRewards>, amount: u64) -> Result<()> {
let staking_seeds = &[b"stake", ctx.accounts.staker.key().as_ref(), &[ctx.accounts.staking_account.bump]];
let cpi_accounts = UpdateRewards {
staking_account: ctx.accounts.staking_account.to_account_info(),
};
let cpi_program = ctx.accounts.rewards_distribution_program.to_account_info();
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, staking_seeds);
rewards_distribution::cpi::update_rewards(cpi_ctx, amount)?;
// Attempt to log the "updated" reward balance
msg!("Rewards: {}", ctx.accounts.staking_account.rewards);
// Logic that uses the stale ctx.accounts.staking_account.rewards
Ok(())
}
#[derive(Accounts)]
pub struct UpdateStakingRewards<'info> {
#[account(mut)]
pub staker: Signer<'info>,
#[account(
mut,
seeds = [b"stake", staker.key().as_ref()],
bump,
)]
pub staking_account: Account<'info, StakingAccount>,
pub rewards_distribution_program: Program<'info, RewardsDistribution>,
}
#[account]
pub struct StakingAccount {
pub staker: Pubkey,
pub stake_amount: u64,
pub rewards: u64,
pub bump: u8,
}Dalam contoh ini, fungsi update_rewards mencoba memperbarui imbalan akun staking pengguna melalui pemanggilan CPI ke program distribusi imbalan. Awalnya, program mencatat ctx.accounts.staking_account.rewards (yaitu saldo imbalan) setelah CPI, lalu melanjutkan ke logika yang menggunakan data ctx.accounts.staking_account.rewards yang sudah lama. Masalahnya adalah status akun staking tidak diperbarui secara otomatis setelah CPI, sehingga datanya sudah tidak mutakhir.
Mitigasi yang Direkomendasikan
Untuk memitigasi masalah ini, panggil metode reload milik Anchor secara eksplisit untuk memuat ulang akun tertentu dari penyimpanan. Memuat ulang akun setelah CPI akan mencerminkan statusnya secara akurat:
pub fn update_rewards(ctx: Context<UpdateStakingRewards>, amount: u64) -> Result<()> {
let staking_seeds = &[b"stake", ctx.accounts.staker.key().as_ref(), &[ctx.accounts.staking_account.bump]];
let cpi_accounts = UpdateRewards {
staking_account: ctx.accounts.staking_account.to_account_info(),
};
let cpi_program = ctx.accounts.rewards_distribution_program.to_account_info();
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, staking_seeds);
rewards_distribution::cpi::update_rewards(cpi_ctx, amount)?;
// Reload the staking account to reflect the updated reward balance
ctx.accounts.staking_account.reload()?;
// Log the updated reward balance
msg!("Rewards: {}", ctx.accounts.staking_account.rewards);
// Logic that uses ctx.accounts.staking_account.rewards
Ok(())
}CPI Arbitrer
Kerentanan
CPI arbitrer terjadi saat suatu program memanggil program lain tanpa memverifikasi identitas program target. Kerentanan ini ada karena runtime Solana memungkinkan program mana pun memanggil program lain jika pemanggil memiliki ID program yang dipanggil dan mematuhi antarmukanya. Jika program melakukan CPI berdasarkan input pengguna tanpa memvalidasi ID program yang dipanggil, program tersebut dapat mengeksekusi kode dalam program yang dikendalikan penyerang.
Contoh Skenario
Pertimbangkan program yang membagikan penghargaan kepada peserta berdasarkan kontribusi mereka pada suatu proyek. Setelah membagikan imbalan, program mencatat detailnya dalam program buku besar terpisah untuk keperluan audit dan pelacakan. Program buku besar tersebut diasumsikan sebagai program tepercaya yang menyediakan antarmuka publik untuk melacak entri tertentu dari program yang diotorisasi. Program ini menyertakan fungsi untuk membagikan dan mencatat imbalan, yang menerima program buku besar sebagai akun. Namun, fungsi tersebut gagal memverifikasi ledger_program yang diberikan sebelum melakukan CPI kepadanya:
pub fn distribute_and_record_rewards(ctx: Context<DistributeAndRecord>, reward_amount: u64) -> ProgramResult {
// Reward distribution logic
let instruction = custom_ledger_program::instruction::record_transaction(
&ctx.accounts.ledger_program.key(),
&ctx.accounts.reward_account.key(),
reward_amount,
)?;
invoke(
&instruction,
&[
ctx.accounts.reward_account.clone(),
ctx.accounts.ledger_program.clone(),
],
)
}
#[derive(Accounts)]
pub struct DistributeAndRecord<'info> {
reward_account: AccountInfo<'info>,
ledger_program: AccountInfo<'info>,
}Penyerang dapat mengeksploitasi hal ini dengan meneruskan ID program berbahaya sebagai ledger_program, sehingga menimbulkan konsekuensi yang tidak diinginkan.
Mitigasi yang Direkomendasikan
Untuk melindungi dari masalah ini, developer dapat menambahkan pemeriksaan yang memverifikasi identitas program buku besar sebelum melakukan CPI. Pemeriksaan ini akan memastikan pemanggilan CPI dilakukan ke program yang dimaksud sehingga mencegah CPI arbitrer:
pub fn distribute_and_record_rewards(ctx: Context<DistributeAndRecord>, reward_amount: u64) -> ProgramResult {
// Reward distribution logic
// Verify the ledger_program is the expected custom ledger program
if ctx.accounts.ledger_program.key() != &custom_ledger_program::ID {
return Err(ProgramError::IncorrectProgramId.into())
}
let instruction = custom_ledger_program::instruction::record_transaction(
&ctx.accounts.ledger_program.key(),
&ctx.accounts.reward_account.key(),
reward_amount,
)?;
invoke(
&instruction,
&[
ctx.accounts.reward_account.clone(),
ctx.accounts.ledger_program.clone(),
],
)
}
#[derive(Accounts)]
pub struct DistributeAndRecord<'info> {
reward_account: AccountInfo<'info>,
ledger_program: AccountInfo<'info>,
}Sebuah program mungkin memiliki modul CPI yang tersedia untuk publik jika ditulis menggunakan Anchor. Ini membuat pemanggilan program dari program Anchor lain menjadi mudah dan aman. Modul CPI Anchor secara otomatis memeriksa bahwa alamat program yang diteruskan cocok dengan alamat program yang tersimpan dalam modul. Sebagai alternatif, melakukan hardcode alamat dapat menjadi solusi alih-alih meminta pengguna meneruskannya.
Fungsionalitas Transfer Otoritas
Kerentanan
Program Solana sering menetapkan kunci publik tertentu sebagai otoritas untuk fungsi penting, seperti memperbarui parameter program atau menarik dana. Namun, ketidakmampuan mentransfer otoritas ini ke alamat lain dapat menimbulkan risiko besar. Keterbatasan ini menjadi masalah dalam skenario seperti perubahan tim, penjualan protokol, atau jika otoritas disusupi.
Contoh Skenario
Pertimbangkan program dengan otoritas admin global yang bertanggung jawab menetapkan parameter protokol tertentu melalui fungsi set_params. Program tersebut tidak menyertakan mekanisme untuk mengubah admin global:
pub fn set_params(ctx: Context<SetParams>, /* parameters to be set */) -> Result<()> {
require_keys_eq!(
ctx.accounts.current_admin.key(),
ctx.accounts.global_admin.authority,
);
// Logic to set parameters
}Di sini, otoritas ditentukan secara statis tanpa kemampuan untuk memperbaruinya ke alamat baru.
Mitigasi yang Direkomendasikan
Pendekatan aman untuk memitigasi masalah ini adalah membuat proses dua langkah untuk mentransfer otoritas. Proses ini memungkinkan otoritas saat ini mencalonkan pending_authority baru yang harus menerima peran tersebut secara eksplisit. Ini tidak hanya menyediakan fungsionalitas transfer otoritas, tetapi juga melindungi dari transfer yang tidak disengaja atau pengambilalihan berbahaya. Alurnya adalah sebagai berikut:
- Pencalonan oleh Otoritas Saat Ini: otoritas saat ini mencalonkan pending_authority baru dengan memanggil nominate_new_authority, yang menetapkan kolom pending_authority dalam status program
- Penerimaan oleh Otoritas Baru: pending_authority yang dicalonkan memanggil accept_authority untuk mengambil peran barunya, sehingga otoritas berpindah dari otoritas saat ini ke pending_authority
Implementasinya akan terlihat kurang lebih seperti ini:
pub fn nominate_new_authority(ctx: Context<NominateAuthority>, new_authority: Pubkey) -> Result<()> {
let state = &mut ctx.accounts.state;
require_keys_eq!(
state.authority,
ctx.accounts.current_authority.key()
);
state.pending_authority = Some(new_authority);
Ok(())
}
pub fn accept_authority(ctx: Context<AcceptAuthority>) -> Result<()> {
let state = &mut ctx.accounts.state;
require_keys_eq!(
Some(ctx.accounts.new_authority.key()),
state.pending_authority
);
state.authority = ctx.accounts.new_authority.key();
state.pending_authority = None;
Ok(())
}
#[derive(Accounts)]
pub struct NominateAuthority<'info> {
#[account(
mut,
has_one = authority,
)]
pub state: Account<'info, ProgramState>,
pub current_authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct AcceptAuthority<'info> {
#[account(
mut,
constraint = state.pending_authority == Some(new_authority.key())
)]
pub state: Account<'info, ProgramState>,
pub new_authority: Signer<'info>,
}
#[account]
pub struct ProgramState {
pub authority: Pubkey,
pub pending_authority: Option<Pubkey>,
// Other relevant program state fields
}Dalam contoh ini, struktur akun ProgramState menyimpan authority saat ini dan pending_authority opsional. Konteks NominateAuthority memastikan bahwa otoritas saat ini menandatangani transaksi sehingga dapat mencalonkan otoritas baru. Konteks AcceptAuthority memeriksa bahwa pending_authority cocok dengan penanda tangan transaksi sehingga pihak tersebut dapat menerima dan menjadi otoritas baru. Konfigurasi ini memastikan transisi otoritas yang aman dan terkendali dalam program.
Kanonisasi Bump Seed
Kerentanan
Kanonisasi bump seed mengacu pada penggunaan bump seed valid tertinggi (yaitu bump kanonis) saat menurunkan PDA. Menggunakan bump kanonis merupakan cara deterministik dan aman untuk menemukan alamat berdasarkan serangkaian seed. Kegagalan menggunakan bump kanonis dapat menimbulkan kerentanan, seperti pelaku berbahaya yang membuat atau memanipulasi PDA sehingga membahayakan logika program atau integritas data.
Contoh Skenario
Pertimbangkan program yang dirancang untuk membuat profil pengguna unik, masing-masing dengan PDA terkait yang secara eksplisit diturunkan menggunakan create_program_address. Program memungkinkan pembuatan profil dengan menerima bump yang diberikan pengguna. Namun, hal ini bermasalah karena menimbulkan risiko penggunaan bump nonkanonis:
pub fn create_profile(ctx: Context<CreateProfile>, user_id: u64, attributes: Vec<u8>, bump: u8) -> Result<()> {
// Explicitly derive the PDA using create_program_address and a user-provided bump
let seeds: &[&[u8]] = &[b"profile", &user_id.to_le_bytes(),&[bump]];
let (derived_address, _bump) = Pubkey::create_program_address(seeds, &ctx.program_id)?;
if derived_address != ctx.accounts.profile.key() {
return Err(ProgramError::InvalidSeeds);
}
let profile_pda = &mut ctx.accounts.profile;
profile_pda.user_id = user_id;
profile_pda.attributes = attributes;
Ok(())
}
#[derive(Accounts)]
pub struct CreateProfile<'info> {
#[account(mut)]
pub user: Signer<'info>,
/// The profile account, expected to be a PDA derived with the user_id and a user-provided bump seed
#[account(mut)]
pub profile: Account<'info, UserProfile>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct UserProfile {
pub user_id: u64,
pub attributes: Vec<u8>,
}Dalam skenario ini, program menurunkan PDA UserProfile menggunakan create_program_address dengan seed yang menyertakan bump dari pengguna. Penggunaan bump dari pengguna bermasalah karena tidak memastikan pemakaian bump kanonis. Hal ini memungkinkan pelaku berbahaya membuat beberapa PDA dengan bump berbeda untuk ID pengguna yang sama.
Mitigasi yang Direkomendasikan
Untuk memitigasi masalah ini, kita dapat memfaktorkan ulang contoh tersebut agar menurunkan PDA menggunakan find_program_address dan memvalidasi bump seed secara eksplisit:
pub fn create_profile(ctx: Context<CreateProfile>, user_id: u64, attributes: Vec<u8>) -> Result<()> {
// Securely derive the PDA using find_program_address to ensure the canonical bump is used
let seeds: &[&[u8]] = &[b"profile", user_id.to_le_bytes()];
let (derived_address, bump) = Pubkey::find_program_address(seeds, &ctx.program_id);
// Store the canonical bump in the profile for future validations
let profile_pda = &mut ctx.accounts.profile;
profile_pda.user_id = user_id;
profile_pda.attributes = attributes;
profile_pda.bump = bump;
Ok(())
}
#[derive(Accounts)]
#[instruction(user_id: u64)]
pub struct CreateProfile<'info> {
#[account(
init,
payer = user,
space = 8 + 1024 + 1,
seeds = [b"profile", user_id.to_le_bytes().as_ref()],
bump
)]
pub profile: Account<'info, UserProfile>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct UserProfile {
pub user_id: u64,
pub attributes: Vec<u8>,
pub bump: u8,
}Di sini, find_program_address digunakan untuk menurunkan PDA dengan bump seed kanonis guna memastikan pembuatan PDA yang deterministik dan aman. Bump kanonis disimpan dalam akun UserProfile, sehingga memungkinkan validasi yang efisien dan aman dalam operasi berikutnya. Kami lebih memilih find_program_address daripada create_program_address karena yang terakhir membuat PDA valid tanpa mencari bump seed. Karena tidak mencari bump seed, fungsi tersebut dapat secara tidak terduga mengembalikan kesalahan untuk serangkaian seed tertentu dan secara umum tidak cocok untuk membuat PDA. find_program_address akan selalu menggunakan bump kanonis saat membuat PDA. Ini karena fungsi tersebut melakukan iterasi melalui berbagai pemanggilan create_program_address, dimulai dengan bump 255 dan menguranginya pada setiap iterasi. Setelah alamat valid ditemukan, fungsi mengembalikan PDA yang diturunkan dan bump kanonis yang digunakan untuk menurunkannya.
Anchor menerapkan bump kanonis untuk penurunan PDA melalui batasan seeds dan bump, yang menyederhanakan seluruh proses ini untuk memastikan pembuatan dan validasi PDA yang aman serta deterministik.
Menutup Akun
Kerentanan
Penutupan akun yang tidak tepat dalam sebuah program dapat menimbulkan beberapa kerentanan, termasuk kemungkinan akun yang telah "ditutup" diinisialisasi ulang atau disalahgunakan. Masalah ini muncul ketika akun tidak ditandai dengan benar sebagai telah ditutup atau penggunaannya kembali dalam transaksi berikutnya tidak dicegah. Kelalaian ini dapat memungkinkan pelaku jahat mengeksploitasi akun tersebut, sehingga menyebabkan tindakan atau akses tidak sah di dalam program.
Contoh Skenario
Pertimbangkan program yang memungkinkan pengguna membuat dan menutup akun penyimpanan data. Program menutup akun dengan memindahkan lamport dari akun tersebut:
pub fn close_account(ctx: Context<CloseAccount>) -> ProgramResult {
let account = ctx.accounts.data_account.to_account_info();
let destination = ctx.accounts.destination.to_account_info();
**destination.lamports.borrow_mut() = destination
.lamports()
.checked_add(account.lamports())
.unwrap();
**account.lamports.borrow_mut() = 0;
Ok(())
}
#[derive(Accounts)]
pub struct CloseAccount<'info> {
#[account(mut)]
pub data_account: Account<'info, Data>,
#[account(mut)]
pub destination: AccountInfo<'info>,
}
#[account]
pub struct Data {
data: u64,
}Hal ini bermasalah karena program tidak mengosongkan data akun atau menandainya sebagai telah ditutup. Hanya memindahkan sisa lamport dari akun tidak akan menutup akun tersebut.
Mitigasi yang Direkomendasikan
Untuk memitigasi masalah ini, program tidak hanya harus memindahkan semua lamport, tetapi juga harus mengosongkan data akun dan menandainya dengan diskriminator (yaitu, "CLOSED_ACCOUNT_DISCRIMINATOR"). Program juga harus menerapkan pemeriksaan untuk mencegah akun yang telah ditutup digunakan kembali dalam transaksi mendatang:
use anchor_lang::__private::CLOSED_ACCOUNT_DISCRIMINATOR;
use anchor_lang::prelude::*;
use std::io::Cursor;
use std::ops::DerefMut;
// Other code
pub fn close_account(ctx: Context<CloseAccount>) -> ProgramResult {
let account = ctx.accounts.data_account.to_account_info();
let destination = ctx.accounts.destination.to_account_info();
**destination.lamports.borrow_mut() = destination
.lamports()
.checked_add(account.lamports())
.unwrap();
**account.lamports.borrow_mut() = 0;
// Zero out the account data
let mut data = account.try_borrow_mut_data()?;
for byte in data.deref_mut().iter_mut() {
*byte = 0;
}
// Mark the account as closed
let dst: &mut [u8] = &mut data;
let mut cursor = Cursor::new(dst);
cursor.write_all(&CLOSED_ACCOUNT_DISCRIMINATOR).unwrap();
Ok(())
}
pub fn force_defund(ctx: Context<ForceDefund>) -> ProgramResult {
let account = &ctx.accounts.account;
let data = account.try_borrow_data()?;
if data.len() < 8 || data[0..8] != CLOSED_ACCOUNT_DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData);
}
let destination = ctx.accounts.destination.to_account_info();
**destination.lamports.borrow_mut() = destination
.lamports()
.checked_add(account.lamports())
.unwrap();
**account.lamports.borrow_mut() = 0;
Ok(())
}
#[derive(Accounts)]
pub struct ForceDefund<'info> {
#[account(mut)]
pub account: AccountInfo<'info>,
#[account(mut)]
pub destination: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct CloseAccount<'info> {
#[account(mut)]
pub data_account: Account<'info, Data>,
#[account(mut)]
pub destination: AccountInfo<'info>,
}
#[account]
pub struct Data {
data: u64,
}Namun, mengosongkan data dan menambahkan diskriminator tertutup saja tidak cukup. Pengguna dapat mencegah akun dibersihkan melalui garbage collection dengan mengembalikan lamport akun sebelum akhir instruksi. Ini akan membuat akun berada dalam kondisi limbo yang aneh sehingga tidak dapat digunakan maupun dibersihkan melalui garbage collection. Karena itu, kami menambahkan fungsi force_defund untuk menangani kasus khusus ini; kini siapa pun dapat mengosongkan dana dari akun yang telah ditutup.
Anchor menyederhanakan proses ini dengan batasan #[account(close = destination)], yang mengotomatiskan penutupan akun secara aman dengan memindahkan lamport, mengosongkan data, dan menetapkan diskriminator akun tertutup, semuanya dalam satu operasi.
Akun Mutable Duplikat
Kerentanan
Akun mutable duplikat merujuk pada skenario ketika akun yang sama diteruskan lebih dari sekali sebagai parameter mutable ke sebuah instruksi. Hal ini terjadi ketika suatu instruksi memerlukan dua akun mutable dengan tipe yang sama. Pelaku jahat dapat meneruskan akun yang sama dua kali, sehingga akun tersebut dimutasi dengan cara yang tidak diinginkan (misalnya, menimpa data). Tingkat keparahan kerentanan ini bergantung pada skenario spesifiknya.
Contoh Skenario
Pertimbangkan program yang dirancang untuk memberi imbalan kepada pengguna berdasarkan partisipasi mereka dalam aktivitas on-chain tertentu. Program tersebut memiliki instruksi untuk memperbarui saldo dua akun: akun imbalan dan akun bonus. Pengguna seharusnya menerima imbalan standar di satu akun dan bonus potensial di akun lain berdasarkan kriteria spesifik yang telah ditentukan:
pub fn distribute_rewards(ctx: Context<DistributeRewards>, reward_amount: u64, bonus_amount: u64) -> Result<()> {
let reward_account = &mut ctx.accounts.reward_account;
let bonus_reward = &mut ctx.accounts.bonus_account;
// Intended to increment the reward and bonus accounts separately
reward_account.balance += reward_amount;
bonus_account.balance += bonus_amount;
Ok(())
}
#[derive(Accounts)]
pub struct DistributeRewards<'info> {
#[account(mut)]
reward_account: Account<'info, RewardAccount>,
#[account(mut)]
bonus_account: Account<'info, RewardAccount>,
}
#[account]
pub struct RewardAccount {
pub balance: u64,
}Jika pelaku jahat meneruskan akun yang sama untuk reward_account dan bonus_account, saldo akun akan diperbarui dua kali secara keliru.
Mitigasi yang Direkomendasikan
Untuk memitigasi masalah ini, tambahkan pemeriksaan dalam logika instruksi guna memverifikasi bahwa public key kedua akun tidak identik:
pub fn distribute_rewards(ctx: Context<DistributeRewards>, reward_amount: u64, bonus_amount: u64) -> Result<()> {
if ctx.accounts.reward_account.key() == ctx.accounts.bonus_account.key() {
return Err(ProgramError::InvalidArgument.into())
}
let reward_account = &mut ctx.accounts.reward_account;
let bonus_reward = &mut ctx.accounts.bonus_account;
// Intended to increment the reward and bonus accounts separately
reward_account.balance += reward_amount;
bonus_account.balance += bonus_amount;
Ok(())
}Developer dapat menggunakan batasan akun Anchor untuk menambahkan pemeriksaan yang lebih eksplisit pada akun. Hal ini dapat dilakukan menggunakan atribut #[account] dan kata kunci constraint:
pub fn distribute_rewards(ctx: Context<DistributeRewards>, reward_amount: u64, bonus_amount: u64) -> Result<()> {
let reward_account = &mut ctx.accounts.reward_account;
let bonus_reward = &mut ctx.accounts.bonus_account;
// Intended to increment the reward and bonus accounts separately
reward_account.balance += reward_amount;
bonus_account.balance += bonus_amount;
Ok(())
}
#[derive(Accounts)]
pub struct DistributeRewards<'info> {
#[account(
mut,
constraint = reward_account.key() != bonus_account.key()
)]
reward_account: Account<'info, RewardAccount>,
#[account(mut)]
bonus_account: Account<'info, RewardAccount>,
}
#[account]
pub struct RewardAccount {
pub balance: u64,
}Frontrunning
Kerentanan
Seiring meningkatnya popularitas bundler transaksi, frontrunning menjadi risiko yang harus ditanggapi secara serius oleh protokol yang dibangun di Solana. Setelah mempool Jito dihapus, frontrunning di sini merujuk pada kemampuan pelaku jahat untuk memanipulasi nilai yang diharapkan dibandingkan nilai aktual melalui transaksi yang dirancang dengan cermat.
Contoh Skenario
Bayangkan protokol yang menangani pembelian dan penawaran untuk suatu produk, dengan menyimpan informasi harga penjual dalam akun bernama SellInfo:
#[derive(Accounts)]
pub struct SellProduct<'info> {
product_listing: Account<'info, ProductListing>,
sale_token_mint: Account<'info, Mint>,
sale_token_destination: Account<'info, TokenAccount>,
product_owner: Signer<'info>,
purchaser_token_source: Account<'info, TokenAccount>,
product: Account<info, Product>
}
#[derive(Accounts)]
pub struct PurchaseProduct<'info> {
product_listing: Account<'info, ProductListing>,
token_destination: Account<'info, TokenAccount>,
token_source: Account<'info, TokenAccount>,
buyer: Signer<'info>,
product_account: Account<'info, Product>,
token_mint_sale: Account<'info, Mint>,
}
#[account]
pub struct ProductListing {
sale_price: u64,
token_mint: Pubkey,
destination_token_account: Pubkey,
product_owner: Pubkey,
product: Pubkey,
}Untuk membeli Product yang terdaftar, pembeli harus meneruskan akun ProductListing yang terkait dengan produk yang diinginkan. Namun, bagaimana jika penjual dapat mengubah sale_price pada listing mereka?
pub fn change_sale_price(ctx: Context<ChangeSalePrice>, new_price: u64) -> Result<()> {...}Hal ini akan menciptakan peluang frontrunning bagi penjual, terutama jika transaksi pembelian pembeli tidak menyertakan pemeriksaan expected_price untuk memastikan mereka tidak membayar lebih dari yang diperkirakan untuk produk yang diinginkan. Jika pembeli mengirimkan transaksi untuk membeli Product tersebut, penjual dapat memanggil change_sale_price dan, menggunakan Jito, memastikan transaksi ini disertakan sebelum transaksi pembeli. Tanpa sepengetahuan pembeli, penjual jahat dapat mengubah harga dalam akun ProductListing menjadi sangat tinggi, sehingga memaksa pembeli membayar jauh lebih mahal daripada yang diperkirakan untuk Product!
Mitigasi yang Direkomendasikan
Solusi sederhananya adalah menyertakan pemeriksaan expected_price di sisi pembelian, sehingga pembeli tidak membayar lebih dari yang diperkirakan untuk Product yang ingin dibeli:
pub fn purchase_product(ctx: Context<PurchaseProduct>, expected_price: u64) -> Result<()> {
assert!(ctx.accounts.product_listing.sale_price <= expected_price);
...
}Inisialisasi yang Tidak Aman
Tidak seperti kontrak yang di-deploy ke EVM, program Solana tidak di-deploy dengan constructor untuk menetapkan variabel state. Sebaliknya, program diinisialisasi secara manual (biasanya oleh fungsi bernama initialize atau nama serupa). Fungsi inisialisasi biasanya menetapkan data seperti otoritas program atau membuat akun yang menjadi dasar program yang di-deploy (yaitu, akun state pusat atau akun sejenis).
Karena fungsi inisialisasi dipanggil secara manual dan bukan secara otomatis saat program di-deploy, instruksi ini harus dipanggil oleh alamat yang diketahui dan berada di bawah kendali tim pengembangan program. Jika tidak, penyerang dapat melakukan frontrunning terhadap inisialisasi dan mungkin menyiapkan program menggunakan akun yang berada di bawah kendali penyerang.
Praktik yang umum adalah menggunakan upgrade_authority program sebagai alamat yang berwenang untuk memanggil fungsi initialize, jika program memiliki upgrade authority.
Contoh Tidak Aman dan Cara Memitigasinya
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.central_state.authority = authority.key();
...
}
#[derive(Accounts)]
pub struct Initialize<'info> {
authority: Signer<'info>,
#[account(mut,
init,
payer = authority,
space = CentralState::SIZE,
seeds = [b"central_state"],
bump
)]
central_state: Account<'info, CentralState>,
...
}
#[account]
pub struct CentralState {
authority: Pubkey,
...
}Contoh di atas adalah fungsi initialize sederhana yang menetapkan otoritas akun CentralState kepada pemanggil instruksi. Namun, pemanggil ini dapat berupa akun apa pun yang memanggil initialize! Seperti yang disebutkan sebelumnya, cara umum untuk mengamankan fungsi inisialisasi adalah menggunakan upgrade_authority program, yang diketahui saat deployment.
Berikut adalah contoh dari dokumentasi Anchor, yang menggunakan constraint untuk memastikan hanya upgrade authority program yang dapat memanggil initialize:
use anchor_lang::prelude::*;
use crate::program::MyProgram;
declare_id!("Cum9tTyj5HwcEiAmhgaS7Bbj4UczCwsucrCkxRECzM4e");
#[program]
pub mod my_program {
use super::*;
pub fn set_initial_admin(
ctx: Context<SetInitialAdmin>,
admin_key: Pubkey
) -> Result<()> {
ctx.accounts.admin_settings.admin_key = admin_key;
Ok(())
}
pub fn set_admin(...){...}
pub fn set_settings(...){...}
}
#[account]
#[derive(Default, Debug)]
pub struct AdminSettings {
admin_key: Pubkey
}
#[derive(Accounts)]
pub struct SetInitialAdmin<'info> {
#[account(init, payer = authority, seeds = [b"admin"], bump)]
pub admin_settings: Account<'info, AdminSettings>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(constraint = program.programdata_address()? == Some(program_data.key()))]
pub program: Program<'info, MyProgram>,
#[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
pub program_data: Account<'info, ProgramData>,
pub system_program: Program<'info, System>,
}Kehilangan Presisi
Kerentanan
Kehilangan presisi, meskipun tampak sangat kecil, dapat menimbulkan ancaman signifikan bagi sebuah program. Hal ini dapat menyebabkan kalkulasi yang keliru, peluang arbitrase, dan perilaku program yang tidak terduga.
Kehilangan presisi dalam operasi aritmetika merupakan sumber kesalahan yang umum. Pada program Solana, aritmetika fixed-point direkomendasikan jika memungkinkan. Hal ini karena program hanya mendukung sebagian kecil operasi float Rust. Jika program mencoba menggunakan operasi float yang tidak didukung, runtime akan mengembalikan unresolved symbol error. Selain itu, operasi float memerlukan lebih banyak instruksi dibandingkan operasi integer yang setara. Penggunaan aritmetika fixed-point serta kebutuhan untuk menangani token dalam jumlah besar dan nilai pecahan secara akurat dapat memperparah kehilangan presisi.
Perkalian Setelah Pembagian
Meskipun sifat asosiatif berlaku untuk sebagian besar operasi matematika, penerapannya dalam aritmetika komputer dapat menyebabkan kehilangan presisi yang tidak terduga. Contoh klasik kehilangan presisi terjadi ketika perkalian dilakukan setelah pembagian, yang dapat menghasilkan nilai berbeda dibandingkan jika perkalian dilakukan sebelum pembagian. Sebagai contoh, pertimbangkan ekspresi berikut: (a / c) * b dan (a * b) / c. Secara matematis, ekspresi ini bersifat asosiatif—keduanya seharusnya menghasilkan nilai yang sama. Namun, dalam konteks Solana dan aritmetika fixed-point, urutan operasi sangat penting. Melakukan pembagian terlebih dahulu (a / c) dapat menyebabkan kehilangan presisi jika hasil bagi dibulatkan ke bawah sebelum dikalikan dengan b. Hasilnya bisa lebih kecil dari yang diperkirakan. Sebaliknya, mengalikan (a * b) sebelum membaginya dengan c dapat mempertahankan lebih banyak presisi awal. Perbedaan ini dapat menyebabkan kalkulasi yang keliru, menciptakan perilaku program yang tidak terduga dan/atau peluang arbitrase.
Fungsi Aritmetika saturating_*
Meskipun fungsi aritmetika saturating_* mencegah overflow dan underflow dengan membatasi nilai pada nilai maksimum atau minimum yang dimungkinkan, fungsi ini dapat menimbulkan bug tersembunyi dan kehilangan presisi jika batas tersebut tercapai secara tidak terduga. Hal ini terjadi ketika logika program mengasumsikan bahwa saturasi saja akan menjamin hasil yang akurat dan mengabaikan penanganan potensi hilangnya presisi atau akurasi.
Sebagai contoh, bayangkan program yang dirancang untuk menghitung dan mendistribusikan imbalan kepada pengguna berdasarkan jumlah token yang mereka perdagangkan dalam periode tertentu:
pub fn calculate_reward(transaction_amount: u64, reward_multiplier: u64) -> u64 {
transaction_amount.saturating_mul(reward_multiplier)
}Pertimbangkan skenario ketika transaction_amount bernilai 100.000 token dan reward_multiplier bernilai 100 token per transaksi. Mengalikan keduanya akan melampaui nilai maksimum yang dapat ditampung u64. Artinya, hasil perkalian akan dibatasi, sehingga menyebabkan kehilangan presisi yang signifikan karena pengguna menerima imbalan lebih sedikit dari yang seharusnya.
Kesalahan Pembulatan
Operasi pembulatan merupakan penyebab umum kehilangan presisi dalam pemrograman. Pemilihan metode pembulatan dapat berdampak signifikan pada akurasi kalkulasi dan perilaku program Solana. Fungsi try_round_u64() membulatkan nilai desimal ke bilangan bulat terdekat. Pembulatan ke atas bermasalah karena dapat meningkatkan nilai secara artifisial, sehingga menimbulkan selisih antara kalkulasi aktual dan yang diharapkan.
Pertimbangkan program Solana yang mengonversi jaminan menjadi likuiditas berdasarkan kondisi pasar. Program menggunakan try_round_u64() untuk membulatkan hasil operasi pembagian:
pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> Result<u64, ProgramError> {
Decimal::from(collateral_amount)
.try_div(self.0)?
.try_round_u64()
}Dalam skenario ini, pembulatan ke atas dapat menyebabkan penerbitan token likuiditas yang melebihi jumlah yang semestinya berdasarkan jaminan. Pelaku jahat dapat mengeksploitasi selisih ini untuk melakukan serangan arbitrase dan mengambil nilai dari protokol melalui hasil pembulatan yang dimanipulasi agar menguntungkan mereka. Untuk memitigasinya, gunakan try_floor_u64 guna membulatkan ke bawah ke bilangan bulat terdekat. Pendekatan ini meminimalkan risiko peningkatan nilai secara artifisial dan memastikan pembulatan tidak memberi pengguna keuntungan dengan merugikan sistem. Sebagai alternatif, terapkan logika untuk menangani skenario ketika pembulatan dapat memengaruhi hasil secara langsung. Hal ini dapat mencakup penetapan ambang tertentu untuk keputusan pembulatan atau penerapan logika berbeda berdasarkan besarnya nilai yang terlibat.
Pemeriksaan Kepemilikan yang Tidak Ada
Kerentanan
Pemeriksaan kepemilikan sangat penting untuk memvalidasi bahwa akun yang terlibat dalam transaksi atau operasi dimiliki oleh program yang diharapkan. Akun memiliki kolom owner, yang menunjukkan program dengan kewenangan untuk menulis ke data akun. Kolom ini memastikan bahwa hanya program yang berwenang yang dapat mengubah state akun. Selain itu, kolom ini berguna untuk memastikan bahwa akun yang diteruskan ke sebuah instruksi dimiliki oleh program yang diharapkan. Tidak adanya pemeriksaan kepemilikan dapat menyebabkan kerentanan serius, termasuk transfer dana tanpa izin dan eksekusi operasi dengan hak istimewa.
Contoh Skenario
Pertimbangkan fungsi program yang dirancang agar hanya admin yang dapat melakukan penarikan dari vault. Fungsi tersebut menerima akun konfigurasi (yaitu, config) dan menggunakan kolom admin untuk memeriksa apakah public key akun admin yang diberikan sama dengan yang tersimpan dalam akun config. Namun, fungsi tersebut tidak memverifikasi kepemilikan akun config karena menganggapnya tepercaya:
pub fn admin_token_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
// Account setup
if config.admin != admin.pubkey() {
return Err(ProgramError::InvalidAdminAccount)
}
// Transfer funds logic
}Pelaku jahat dapat mengeksploitasi hal ini dengan memberikan akun config yang mereka kendalikan dengan kolom admin yang cocok, sehingga mengelabui program agar mengeksekusi penarikan.
Mitigasi yang Direkomendasikan
Untuk memitigasi hal ini, lakukan pemeriksaan kepemilikan yang memverifikasi kolom owner akun:
pub fn admin_token_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
// Account setup
if config.admin != admin.pubkey() {
return Err(ProgramError::InvalidAdminAccount)
}
if config.owner != program_id {
return Err(ProgramError::InvalidConfigAccount)
}
// Transfer funds logic
}Anchor menyederhanakan pemeriksaan ini dengan tipe Account. Account<'info, T> adalah wrapper untuk AccountInfo, yang memverifikasi kepemilikan program dan melakukan deserialisasi data yang mendasarinya menjadi T (yaitu, tipe akun yang ditentukan). Hal ini memungkinkan developer menggunakan Account<'info, T> untuk memvalidasi kepemilikan akun dengan mudah. Developer juga dapat menggunakan atribut #[account] untuk menambahkan trait Owner ke akun tertentu. Trait ini menentukan alamat yang diharapkan memiliki akun tersebut. Selain itu, developer dapat menggunakan constraint owner untuk menentukan program yang seharusnya memiliki akun tertentu jika berbeda dari program yang sedang dieksekusi. Hal ini berguna, misalnya, ketika menulis instruksi yang mengharapkan sebuah akun menjadi PDA yang berasal dari program lain. Constraint owner didefinisikan sebagai #[account(owner = <expr>)], dengan <expr> sebagai ekspresi arbitrer.
Akun Read-Only
Memverifikasi validitas akun yang ditetapkan sebagai read-only dalam konteks eksekusi program juga sama pentingnya. Hal ini krusial karena pelaku jahat dapat meneruskan akun dengan data arbitrer atau data yang telah direkayasa sebagai pengganti akun yang sah. Ini dapat menyebabkan perilaku program yang tidak terduga atau berbahaya. Developer tetap harus melakukan pemeriksaan untuk memastikan bahwa akun yang perlu dibaca program adalah akun asli dan tidak dimanipulasi. Hal ini dapat mencakup verifikasi alamat akun terhadap nilai yang diketahui atau konfirmasi bahwa owner akun sesuai dengan yang diharapkan, terutama untuk sysvar (yaitu, akun sistem read-only, seperti Clock atau EpochSchedule). Akses sysvar menggunakan metode get(), yang tidak memerlukan pemeriksaan alamat atau kepemilikan secara manual. Ini merupakan pendekatan yang lebih aman untuk mengakses akun tersebut; namun, tidak semua sysvar mendukung metode get(). Dalam kasus ini, akses sysvar menggunakan alamat publiknya.
Pemeriksaan Penanda Tangan yang Tidak Ada
Kerentanan
Transaksi ditandatangani dengan private key dompet untuk memastikan autentikasi, integritas, nonrepudiasi, dan otorisasi transaksi tertentu oleh dompet tertentu. Dengan mewajibkan transaksi ditandatangani menggunakan private key pengirim, runtime Solana dapat memverifikasi bahwa akun yang tepat memulai transaksi dan transaksi tersebut tidak dimanipulasi. Mekanisme ini mendasari sifat trustless jaringan terdesentralisasi. Tanpa verifikasi ini, akun apa pun yang memberikan akun yang benar sebagai argumen dapat mengeksekusi transaksi. Hal ini dapat menyebabkan akses tanpa izin ke informasi, dana, atau fungsionalitas dengan hak istimewa. Kerentanan ini muncul karena program tidak memvalidasi apakah suatu operasi telah ditandatangani oleh private key akun yang tepat sebelum mengeksekusi fungsionalitas dengan hak istimewa tertentu.
Contoh Skenario
Perhatikan fungsi berikut:
pub fn update_admin(program_id: &Pubkey, accounts &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let config = ConfigAccount::unpack(next_account_info(account_iter)?)?;
let admin = next_account_info(account_iter)?;
let new_admin = next_account_info (account_iter)?;
if admin.pubkey() != config.admin {
return Err(ProgramError::InvalidAdminAccount);
}
config.admin = new_admin.pubkey();
Ok(())
}Fungsi ini dimaksudkan untuk memperbarui admin program. Fungsi tersebut menyertakan pemeriksaan untuk memastikan bahwa admin saat ini yang memulai operasi, yang merupakan kontrol akses yang baik. Namun, fungsi tersebut tidak memverifikasi bahwa private key admin saat ini menandatangani transaksi. Dengan demikian, siapa pun yang memanggil fungsi ini dapat meneruskan akun admin yang tepat sehingga admin.pubkey() = config.admin, terlepas dari apakah akun yang memanggil fungsi ini benar-benar merupakan admin saat ini. Hal ini memungkinkan pelaku jahat mengeksekusi instruksi dengan meneruskan akun mereka sebagai admin baru, sehingga secara langsung melewati kebutuhan akan otorisasi admin saat ini.
Mitigasi yang Direkomendasikan
Program harus menyertakan pemeriksaan untuk memverifikasi bahwa akun telah ditandatangani oleh dompet yang tepat. Hal ini dapat dilakukan dengan memeriksa kolom AccountInfo::is_signer dari akun yang terlibat dalam transaksi. Program dapat memastikan hanya akun yang berwenang yang dapat melakukan tindakan tertentu dengan memeriksa apakah akun yang mengeksekusi operasi dengan hak istimewa memiliki flag is_signer yang ditetapkan ke true.
Contoh kode yang telah diperbarui akan terlihat seperti berikut:
pub fn update_admin(program_id: &Pubkey, accounts &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let config = ConfigAccount::unpack(next_account_info(account_iter)?)?;
let admin = next_account_info(account_iter)?;
let new_admin = next_account_info (account_iter)?;
if admin.pubkey() != config.admin {
return Err(ProgramError::InvalidAdminAccount);
}
// Add in a check for the admin's signature
if !admin.is_signer {
return Err(ProgramError::NotSigner);
}
config.admin = new_admin.pubkey();
Ok(())
}Anchor menyederhanakan seluruh proses ini dengan tipe akun Signer<’info>.
Overflow dan Underflow
Kerentanan
Integer adalah angka tanpa komponen pecahan. Rust menyimpan integer sebagai variabel berukuran tetap. Variabel ini ditentukan oleh signedness (yaitu, signed atau unsigned) dan jumlah ruang yang ditempatinya di memori. Misalnya, tipe u8 menunjukkan integer unsigned yang menempati ruang 8 bit. Tipe ini dapat menyimpan nilai dari 0 hingga 255. Menyimpan nilai di luar rentang tersebut akan mengakibatkan overflow atau underflow integer. Overflow integer terjadi ketika variabel melampaui kapasitas maksimumnya dan kembali ke nilai minimumnya. Underflow integer terjadi ketika variabel turun di bawah kapasitas minimumnya dan kembali ke nilai maksimumnya.
Rust menyertakan pemeriksaan overflow dan underflow integer saat melakukan kompilasi dalam mode debug. Pemeriksaan ini akan menyebabkan program mengalami panic saat runtime jika kondisi tersebut terdeteksi. Namun, Rust tidak menyertakan pemeriksaan yang menyebabkan panic untuk overflow dan underflow integer saat melakukan kompilasi dalam mode release dengan flag --release. Perilaku ini dapat menimbulkan kerentanan yang sulit terlihat karena overflow atau underflow terjadi tanpa peringatan. Toolchain Berkley Packet Filter (BPF) merupakan bagian penting dari lingkungan pengembangan Solana karena digunakan untuk mengompilasi program Solana. Perintah cargo build-bpf mengompilasi proyek Rust menjadi bytecode BPF untuk deployment. Masalahnya, perintah ini secara default mengompilasi program dalam mode release. Karena itu, program Solana rentan terhadap overflow dan underflow integer.
Contoh Skenario
Penyerang dapat mengeksploitasi kerentanan ini dengan memanfaatkan perilaku overflow/underflow tanpa peringatan dalam mode release, terutama pada fungsi yang menangani saldo token. Perhatikan contoh berikut:
pub fn process_instruction(
_program_id: & Pubkey,
accounts: [&AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let account = next_account_info(account_info_iter)?;
let mut balance: u8 = account.data.borrow()[0];
let tokens_to_subtract: u8 = 100;
balance = balance - tokens_to_subtract;
account.data.borrow_mut()[0] = balance;
msg!("Updated balance to {}", balance);
Ok(())
}Agar sederhana, fungsi ini mengasumsikan saldo disimpan dalam byte pertama. Fungsi tersebut mengambil saldo akun dan mengurangkan tokens_to_subtract darinya. Jika saldo pengguna lebih kecil daripada tokens_to_subtract, underflow akan terjadi. Misalnya, pengguna dengan 10 token akan mengalami underflow sehingga saldo totalnya menjadi 165 token.
Mitigasi yang Direkomendasikan
overflow-checks
Cara termudah untuk memitigasi kerentanan ini adalah menetapkan key overflow-checks ke true dalam file Cargo.toml proyek. Dengan demikian, Rust akan menambahkan pemeriksaan overflow dan underflow pada compiler. Namun, penambahan pemeriksaan overflow dan underflow meningkatkan biaya komputasi suatu transaksi. Jika komputasi perlu dioptimalkan, menetapkan overflow-checks ke false mungkin lebih bermanfaat.
Aritmetika checked_*
Gunakan fungsi aritmetika checked_* milik Rust pada setiap tipe integer untuk memeriksa overflow dan underflow secara strategis di seluruh program Anda. Fungsi ini akan mengembalikan None jika terjadi overflow atau underflow. Dengan demikian, program dapat menangani error dengan baik. Misalnya, Anda dapat melakukan refactor pada kode sebelumnya menjadi:
pub fn process_instruction(
_program_id: & Pubkey,
accounts: [&AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let account = next_account_info(account_info_iter)?;
let mut balance: u8 = account.data.borrow()[0];
let tokens_to_subtract: u8 = 100;
match balance.checked_sub(tokens_to_subtract) {
Some(new_balance) => {
account.data.borrow_mut()[0] = new_balance;
msg!("Updated balance to {}", new_balance);
},
None => {
return Err(ProgramErrorr::InsufficientFunds);
}
}
Ok(())
}Dalam contoh yang telah direvisi, checked_sub digunakan untuk mengurangkan tokens_to_subtract dari balance. Jadi, jika balance cukup untuk pengurangan tersebut, checked_sub akan mengembalikan Some(new_balance). Program kemudian memperbarui saldo akun dengan aman dan mencatatnya dalam log. Namun, jika pengurangan akan menghasilkan underflow, checked_sub mengembalikan None, yang dapat kita tangani dengan mengembalikan error.
Macro Checked Math
Checked Math adalah procedural macro untuk mengubah properti pemeriksaan ekspresi matematika tanpa mengubah ekspresi tersebut dalam sebagian besar kasus. Masalah pada fungsi aritmetika checked_* adalah hilangnya notasi matematika. Sebagai gantinya, metode rumit seperti a.checked_add(b).unwrap() harus digunakan, bukan a + b. Misalnya, jika ingin menulis (x * y) + z menggunakan fungsi aritmetika checked, kita harus menulis x.checked_mul(y).unwrap().checked_add(z).unwrap().
Dengan macro Checked Math, ekspresi tersebut akan terlihat seperti berikut:
use checked_math::checked_math as cm;
cm!((x * y) + z).unwrap()Cara ini lebih praktis untuk ditulis, mempertahankan notasi matematika ekspresi, dan hanya memerlukan satu .unwrap(). Alasannya, macro mengubah ekspresi matematika biasa menjadi ekspresi yang mengembalikan None jika salah satu langkah yang diperiksa mengembalikan None. Jika berhasil, Some(_) akan dikembalikan. Karena itulah kita melakukan unwrap pada ekspresi di bagian akhir.
Casting
Demikian pula, casting antar-tipe integer menggunakan keyword as tanpa pemeriksaan yang tepat dapat menimbulkan kerentanan overflow atau underflow integer. Hal ini karena casting dapat memotong atau memperluas nilai dengan cara yang tidak diinginkan. Saat melakukan casting dari tipe integer yang lebih besar ke tipe yang lebih kecil (misalnya, u64 ke u32), Rust akan memotong bit tinggi dari nilai asli yang tidak muat dalam tipe target. Ini menjadi masalah ketika nilai asli melampaui nilai maksimum yang dapat disimpan oleh tipe target. Saat melakukan casting dari tipe integer yang lebih kecil ke tipe yang lebih besar (misalnya, i16 ke i32), Rust akan memperluas nilai. Proses ini sederhana untuk tipe unsigned. Namun, pada integer signed, proses ini dapat menyebabkan sign extension yang menghasilkan nilai negatif yang tidak diinginkan.
Mitigasi yang Direkomendasikan
Gunakan metode casting aman milik Rust untuk memitigasi kerentanan ini. Metode tersebut mencakup try_from dan from. Penggunaan try_from mengembalikan tipe Result, sehingga kasus ketika nilai tidak muat dalam tipe target dapat ditangani secara eksplisit dengan baik. Metode from milik Rust dapat digunakan untuk konversi implisit yang aman dan dijamin tidak menyebabkan kehilangan data (misalnya, u8 ke u32). Misalnya, anggap sebuah program perlu mengonversi jumlah token u64 secara aman ke tipe u32 untuk diproses. Dalam hal ini, program dapat melakukan hal berikut:
pub fn convert_token_amount(amount: u64) -> Result<u32, ProgramError> {
u32::try_from(amount).map_err(|_| ProgramError::InvalidArgument)
}Dalam contoh ini, jika amount melampaui nilai maksimum yang dapat ditampung oleh u32 (yaitu, 4 294 967 295), konversi akan gagal dan program mengembalikan error. Hal ini mencegah terjadinya potensi overflow/underflow.
Berbagi PDA
Kerentanan
Berbagi PDA adalah kerentanan umum yang muncul ketika PDA yang sama digunakan di beberapa domain otoritas atau peran. Hal ini dapat memungkinkan pelaku jahat mengakses data atau dana yang bukan miliknya melalui penyalahgunaan PDA sebagai signer tanpa pemeriksaan kontrol akses yang tepat.
Contoh Skenario
Perhatikan program yang dirancang untuk memfasilitasi staking token dan distribusi reward. Program tersebut menggunakan satu PDA untuk mentransfer token ke pool tertentu dan menarik reward. PDA diturunkan menggunakan seed statis (misalnya, nama staking pool), sehingga digunakan bersama di seluruh operasi:
pub fn stake_tokens(ctx: Context<StakeTokens>, amount: u64) -> ProgramResult {
// Logic to stake tokens
Ok(())
}
pub fn withdraw_rewards(ctx: Context<WithdrawRewards>, amount: u64) -> ProgramResult {
// Logic to withdraw rewards
Ok(())
}
#[derive(Accounts)]
pub struct StakeTokens<'info> {
#[account(
mut,
seeds = [b"staking_pool_pda"],
bump
)]
staking_pool: AccountInfo<'info>,
// Other staking-related accounts
}
#[derive(Accounts)]
pub struct WithdrawRewards<'info> {
#[account(
mut,
seeds = [b"staking_pool_pda"],
bump
)]
rewards_pool: AccountInfo<'info>,
// Other rewards withdrawal-related accounts
}Hal ini bermasalah karena fungsi staking dan penarikan reward mengandalkan PDA yang sama, yang diturunkan dari staking_pool_pda. Kondisi ini dapat memungkinkan pengguna memanipulasi kontrak untuk melakukan penarikan reward tanpa izin atau memanipulasi staking.
Mitigasi yang Direkomendasikan
Untuk memitigasi kerentanan ini, gunakan PDA yang berbeda untuk fungsi yang berbeda. Pastikan setiap PDA melayani konteks tertentu dan diturunkan menggunakan seed unik yang khusus untuk setiap operasi:
pub fn stake_tokens(ctx: Context<StakeTokens>, amount: u64) -> ProgramResult {
// Logic to stake tokens
Ok(())
}
pub fn withdraw_rewards(ctx: Context<WithdrawRewards>, amount: u64) -> ProgramResult {
// Logic to withdraw rewards
Ok(())
}
#[derive(Accounts)]
pub struct StakeTokens<'info> {
#[account(
mut,
seeds = [b"staking_pool", &staking_pool.key().as_ref()],
bump
)]
staking_pool: AccountInfo<'info>,
// Other staking-related accounts
}
#[derive(Accounts)]
pub struct WithdrawRewards<'info> {
#[account(
mut,
seeds = [b"rewards_pool", &rewards_pool.key().as_ref()],
bump
)]
rewards_pool: AccountInfo<'info>,
// Other rewards withdrawal-related accounts
}Dalam contoh di atas, PDA untuk staking token dan penarikan reward diturunkan menggunakan seed yang berbeda (masing-masing staking_pool dan rewards_pool) serta digabungkan dengan key akun tertentu. Hal ini memastikan PDA terikat secara unik pada fungsi yang dituju, sehingga mengurangi risiko tindakan tanpa izin.
Akun Tersisa
Kerentanan
ctx.remaining_accounts menyediakan cara untuk meneruskan akun tambahan ke suatu fungsi yang awalnya tidak ditentukan dalam struct Accounts. Hal ini memberi developer fleksibilitas lebih besar untuk menangani skenario yang memerlukan jumlah akun dinamis (yaitu, memproses jumlah pengguna yang bervariasi atau berinteraksi dengan program yang berbeda. Namun, peningkatan fleksibilitas ini memiliki kelemahan: akun yang diteruskan melalui ctx.remaining_accounts tidak menjalani validasi yang sama seperti akun yang ditentukan dalam struct Accounts. Karena ctx.remaining_accounts tidak memvalidasi akun yang diteruskan, pelaku jahat dapat mengeksploitasinya dengan meneruskan akun yang tidak dimaksudkan untuk berinteraksi dengan program, sehingga menimbulkan tindakan atau akses tanpa izin.
Contoh Skenario
Perhatikan program reward yang menggunakan ctx.remaining_accounts untuk menerima PDA pengguna dan menghitung reward secara dinamis:
pub fn calculate_rewards(ctx: Context<CalculateRewards>) -> Result<()> {
let rewards_account = &ctx.accounts.rewards_account;
let authority = &ctx.accounts.authority;
// Iterate over accounts passed in via ctx.remaining_accounts
for user_pda_info in ctx.remaining_accounts.iter() {
// logic to check user activity and calculate rewards
}
// Logic to distribute calculated rewards
Ok(())
}
#[derive(Accounts)]
pub struct CalculateRewards<'info> {
#[account(mut)]
pub rewards_account: Account<'info, RewardsAccount>,
pub authority : Signer<'info>,
}
#[account]
pub struct RewardsAccount {
pub total_rewards: u64,
// Other relevant fields
}Masalahnya, tidak ada pemeriksaan eksplisit untuk memvalidasi akun yang diteruskan melalui ctx.remaining_accounts. Akibatnya, program tidak dapat memastikan bahwa hanya akun milik pengguna yang valid dan memenuhi syarat yang diproses dalam penghitungan dan distribusi reward. Karena itu, pelaku jahat dapat meneruskan akun yang bukan miliknya atau akun yang dibuat sendiri untuk menerima reward lebih besar daripada yang semestinya.
Mitigasi yang Direkomendasikan
Untuk memitigasi kerentanan ini, developer harus memverifikasi validitas setiap akun secara manual di dalam fungsi. Pemeriksaan ini mencakup verifikasi owner akun untuk memastikan kecocokannya dengan pengguna yang diharapkan serta validasi data terkait di dalam akun. Dengan menyertakan pemeriksaan manual ini, developer dapat memanfaatkan fleksibilitas ctx.remaining_acocunts sekaligus mengurangi risiko akses atau manipulasi tanpa izin.
Error Khusus Rust
Rust adalah lingua franca dalam pengembangan program di Solana. Pengembangan dengan Rust menghadirkan serangkaian tantangan dan pertimbangan tersendiri, terutama terkait kode unsafe dan error khusus Rust. Memahami berbagai batasan Rust membantu Anda mengembangkan program yang aman, efisien, dan andal.
Unsafe Rust
Rust dikenal karena jaminan keamanan memorinya, yang dicapai melalui sistem ownership dan borrowing yang ketat. Namun, jaminan ini terkadang dapat menjadi penghalang. Karena itu, Rust menyediakan keyword unsafe untuk melewati pemeriksaan keamanan. Rust unsafe digunakan dalam empat konteks utama:
- Fungsi Unsafe: fungsi yang menjalankan operasi yang mungkin melanggar jaminan keamanan Rust harus ditandai dengan keyword unsafe. Misalnya, unsafe fn dangerous_function() {}
- Blok Unsafe: blok kode yang mengizinkan operasi unsafe. Misalnya, unsafe { // Operasi unsafe }
- Trait Unsafe: trait yang menyiratkan invariant tertentu yang tidak dapat diverifikasi oleh compiler. Misalnya, unsafe trait BadTrait {}
- Mengimplementasikan Trait Unsafe: implementasi trait unsafe juga harus ditandai sebagai unsafe. Misalnya, unsafe impl UnsafeTrait for UnsafeType {}
Unsafe Rust tersedia karena analisis statis bersifat konservatif. Saat compiler mencoba menentukan apakah kode memenuhi serangkaian jaminan tertentu, lebih baik menolak beberapa kode valid daripada menerima beberapa kode yang tidak valid. Meskipun kode dapat berjalan dengan baik, compiler Rust akan menolaknya jika tidak memiliki cukup informasi untuk memastikan apakah kode tersebut memenuhi jaminan keamanan Rust. Kode unsafe memungkinkan developer melewati pemeriksaan ini dengan menanggung risikonya sendiri. Selain itu, hardware komputer pada dasarnya tidak aman. Developer harus diizinkan menjalankan operasi unsafe agar dapat melakukan pemrograman tingkat rendah dengan Rust.
Dengan keyword unsafe, developer dapat:
- Melakukan Dereference pada Raw Pointer: memungkinkan akses memori langsung ke raw pointer yang dapat menunjuk ke lokasi memori mana pun, yang mungkin tidak menyimpan data valid
- Memanggil Fungsi Unsafe: fungsi ini mungkin tidak mematuhi jaminan keamanan Rust dan dapat menimbulkan perilaku yang tidak terdefinisi
- Mengakses Variabel Statis Mutable: state global yang mutable dapat menyebabkan data race
Cara terbaik untuk memitigasi risiko Unsafe Rust adalah meminimalkan penggunaan blok unsafe. Jika kode unsafe benar-benar diperlukan karena alasan apa pun, pastikan kode tersebut terdokumentasi dengan baik, diaudit secara rutin, dan, jika memungkinkan, dienkapsulasi dalam abstraksi aman yang dapat digunakan oleh bagian program lainnya.
Panic dan Pengelolaan Error
Panic terjadi ketika program Rust menemukan error yang tidak dapat dipulihkan dan menghentikan eksekusi. Panic digunakan untuk error tak terduga yang tidak dimaksudkan untuk ditangkap. Dalam konteks program Solana, panic dapat menyebabkan perilaku tak terduga karena runtime mengharapkan program menangani error dengan baik tanpa mengalami crash.
Saat panic terjadi, Rust mulai melakukan unwinding dan membersihkan stack selama proses tersebut. Proses ini menghasilkan stack trace yang berisi informasi terperinci tentang error terkait. Informasi ini dapat memberikan petunjuk kepada penyerang mengenai struktur file yang mendasarinya. Meskipun hal ini tidak berlaku langsung pada program Solana, dependency yang digunakan program mungkin rentan terhadap serangan semacam itu. Pastikan dependency selalu diperbarui dan gunakan versi yang tidak memiliki kerentanan yang diketahui.
Skenario panic yang umum meliputi:
- Pembagian dengan Nol: Rust akan mengalami panic saat mencoba membagi dengan nol. Karena itu, selalu periksa apakah divisor bernilai nol sebelum melakukan pembagian
- Indeks Array di Luar Batas: mengakses array dengan indeks yang melampaui batasnya akan menyebabkan panic. Untuk memitigasinya, gunakan metode yang mengembalikan tipe Option (seperti get) agar elemen array dapat diakses dengan aman
- Melakukan Unwrap pada Nilai None: memanggil .unwrap() pada Option yang menyimpan nilai None akan menyebabkan panic. Selalu gunakan pattern matching atau metode seperti unwrap_or, unwrap_or_else, atau operator ? dalam fungsi yang mengembalikan Result
Untuk memitigasi masalah yang berkaitan dengan panic, Anda harus menghindari operasi yang menyebabkan panic, memvalidasi semua input dan kondisi yang dapat menimbulkan operasi bermasalah, serta menggunakan tipe Result dan Option untuk penanganan error. Selain itu, menulis pengujian program yang menyeluruh akan membantu menemukan dan mengatasi potensi skenario panic sebelum deployment.
Tabrakan Seed
Kerentanan
Tabrakan seed terjadi ketika input yang berbeda (yaitu, seed dan ID program) yang digunakan untuk menghasilkan PDA justru menghasilkan alamat PDA yang sama. Hal ini bermasalah ketika PDA digunakan dalam suatu program untuk tujuan yang berbeda karena dapat menyebabkan perilaku tak terduga, termasuk serangan denial of service atau kompromi penuh.
Contoh Skenario
Perhatikan program untuk platform voting terdesentralisasi bagi berbagai proposal dan inisiatif. Setiap sesi voting untuk proposal atau inisiatif tertentu dibuat dengan identifier unik, lalu pengguna memberikan suara. Program menggunakan PDA untuk sesi voting dan suara individual:
// Creating a Voting Session PDA
#[derive(Accounts)]
#[instruction(session_id: String)]
pub struct CreateVotingSession<'info> {
#[account(mut)]
pub organizer: Signer<'info>,
#[account(
init,
payer = organizer,
space = 8 + Product::SIZE,
seeds = [b"session", session_id.as_bytes()],
)]
pub voting_session: Account<'info, VotingSession>,
pub system_program: Program<'info, System>,
}
// Submitting a Vote PDA
#[derive(Accounts)]
#[instruction(session_id: String)]
pub struct SubmitVote<'info> {
#[account(mut)]
pub voter: Signer<'info>,
#[account(
init,
payer = voter,
space = 8 + Vote::SIZE,
seeds = [session_id.as_bytes(), voter.key().as_ref()]
)]
pub vote: Account<'info, Vote>,
pub system_program: Program<'info, System>,
}Dalam skenario ini, penyerang akan mencoba merancang sesi voting secara cermat agar, ketika digabungkan dengan seed statis "session", menghasilkan PDA yang kebetulan cocok dengan PDA yang dihasilkan untuk sesi voting lain. Pembuatan PDA secara sengaja yang bertabrakan dengan PDA sesi voting lain dapat mengganggu operasi platform. Misalnya, hal ini dapat mencegah pemberian suara yang sah untuk proposal atau menghalangi penambahan inisiatif baru ke platform karena runtime Solana tidak dapat membedakan PDA yang bertabrakan.
Mitigasi yang Direkomendasikan
Untuk memitigasi risiko tabrakan seed, developer dapat:
- Menggunakan prefiks unik untuk seed di berbagai PDA dalam program yang sama. Pendekatan ini membantu memastikan bahwa setiap PDA tetap berbeda
- Menggunakan identifier unik (misalnya, timestamp, ID pengguna, nilai nonce) untuk menjamin PDA unik dihasilkan setiap saat
- Memvalidasi secara terprogram bahwa PDA yang dihasilkan tidak bertabrakan dengan PDA yang sudah ada
Type Cosplay
Kerentanan
Type cosplay adalah kerentanan ketika suatu tipe akun disalahartikan sebagai tipe lain karena tidak adanya pemeriksaan tipe selama deserialisasi. Hal ini dapat menyebabkan pelaksanaan tindakan tanpa izin atau kerusakan data karena program beroperasi berdasarkan asumsi yang salah mengenai peran atau izin akun. Selalu periksa secara eksplisit tipe akun yang dimaksud selama deserialisasi.
Contoh Skenario
Perhatikan program yang mengelola akses ke operasi admin berdasarkan peran pengguna. Setiap akun pengguna menyertakan discriminator peran untuk membedakan pengguna biasa dan administrator. Program tersebut memiliki fungsi untuk memperbarui pengaturan admin yang hanya ditujukan bagi administrator. Namun, program gagal memeriksa discriminator akun dan melakukan deserialisasi data akun pengguna tanpa memastikan apakah akun tersebut milik administrator:
pub fn update_admin_settings(ctx: Context<UpdateSettings>) -> ProgramResult {
// Deserialize without checking the discriminator
let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
// Sensitive update logic
msg!("Admin settings updated by: {}", user.authority)
Ok(())
}
#[derive(Accounts)]
pub struct UpdateSettings<'info> {
user: AccountInfo<'info>
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
authority: Pubkey,
}Masalahnya, update_admin_settings melakukan deserialisasi pada akun pengguna yang diteruskan tanpa memeriksa discriminator peran akun tersebut. Salah satu penyebabnya adalah struct User tidak memiliki field discriminator!
Mitigasi yang Direkomendasikan
Untuk memitigasi masalah ini, developer dapat menambahkan field discriminator ke struct User dan memverifikasinya selama proses deserialisasi:
pub fn update_admin_settings(ctx: Context<UpdateSettings>) -> ProgramResult {
let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
// Verify the user's discriminator
if user.discriminant != AccountDiscriminant::Admin {
return Err(ProgramError::InvalidAccountData.into())
}
// Sensitive update logic
msg!("Admin settings updated by: {}", user.authority)
Ok(())
}
#[derive(Accounts)]
pub struct UpdateSettings<'info> {
user: AccountInfo<'info>
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
discriminant: AccountDiscriminant,
authority: Pubkey,
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq)]
pub enum AccountDiscriminant {
Admin,
// Other account types
}Anchor menyederhanakan mitigasi kerentanan type cosplay dengan mengelola discriminator untuk tipe akun secara otomatis. Hal ini dilakukan melalui wrapper Account<'info, T>, tempat Anchor memastikan keamanan tipe dengan memeriksa discriminator secara otomatis selama deserialisasi. Dengan demikian, developer dapat lebih berfokus pada logika bisnis program daripada menerapkan berbagai pemeriksaan tipe secara manual.
Kesimpulan
Pentingnya keamanan program tidak dapat dilebih-lebihkan. Artikel ini telah membahas berbagai kerentanan umum, mulai dari kesalahan khusus Rust hingga kompleksitas metode realloc milik Anchor. Proses untuk menguasai setiap kerentanan ini, serta keamanan program secara umum, terus berlanjut dan membutuhkan pembelajaran, adaptasi, serta kolaborasi tanpa henti. Sebagai developer, komitmen kita terhadap keamanan bukan sekadar melindungi aset, tetapi juga membangun kepercayaan, memastikan integritas aplikasi, serta berkontribusi terhadap pertumbuhan dan stabilitas Solana.
Jika Anda sudah membaca sejauh ini, terima kasih, anon! Pastikan Anda memasukkan alamat email di bawah agar tidak pernah melewatkan kabar terbaru tentang Solana. Siap mempelajari lebih lanjut? Jelajahi artikel terbaru di blog Helius dan lanjutkan perjalanan Solana Anda hari ini.
Sumber Daya Tambahan
Artikel Terkait
Berlangganan Helius
Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan


