A comprehensive collection of Solana program security vulnerabilities and their fixes, built with the Anchor framework. Each example demonstrates a vulnerable implementation alongside a secure alternative, with detailed explanations for educational purposes.
This repository contains 11 security vulnerability examples commonly found in Solana programs. Each folder contains a complete Anchor program demonstrating:
β οΈ Vulnerable implementation - How the bug manifests- β Secure implementation - The proper fix
- π Detailed README - Explanation, attack scenarios, and prevention patterns
| # | Vulnerability | Severity | Description |
|---|---|---|---|
| 0 | Missing Signer Check | CRITICAL | Using AccountInfo instead of Signer allows unauthorized access |
| 1 | Arithmetic Overflow | HIGH | Unchecked math operations can wrap around, causing incorrect calculations |
| 2 | Account Revival | HIGH | Improperly closed accounts can be revived by re-funding them |
| 3 | Missing Owner Check | CRITICAL | Not verifying account ownership allows fake account injection |
| 4 | Remaining Accounts | CRITICAL | Unvalidated remaining_accounts can contain malicious data |
| 5 | Unverified CPI | CRITICAL | Cross-program invocations to unverified programs |
| 6 | Missing Discriminator | HIGH | Type confusion from skipping account discriminator validation |
| 7 | Type Casting | MEDIUM | Unsafe as casts silently truncate values |
| 8 | Initialization DoS | MEDIUM | Front-running init constraints to block users |
| 9 | TOCTOU | HIGH | Time-of-check vs time-of-use state inconsistency |
| 10 | Missing Rent Check | HIGH | Accounts below rent-exempt threshold get purged |
Problem: Using AccountInfo doesn't verify the account actually signed the transaction.
// β οΈ VULNERABLE
pub authority: AccountInfo<'info>, // Anyone can pass any pubkey!
// β
SECURE
pub authority: Signer<'info>, // Anchor verifies signatureProblem: Using -, +, * operators can silently overflow/underflow.
// β οΈ VULNERABLE
balance = balance - amount; // Wraps on underflow!
// β
SECURE
balance = balance.checked_sub(amount).ok_or(ErrorCode::Underflow)?;Problem: Zeroing lamports without clearing data allows account reactivation.
// β οΈ VULNERABLE
**account.try_borrow_mut_lamports()? = 0; // Data remains!
// β
SECURE
#[account(mut, close = recipient)]
pub account: Account<'info, MyData>, // Anchor zeros everythingProblem: Not verifying account owner allows fake accounts from other programs.
// β οΈ VULNERABLE
pub vault: AccountInfo<'info>, // Could be owned by anyone!
// β
SECURE
pub vault: Account<'info, Vault>, // Anchor verifies ownerProblem: Iterating remaining_accounts without validation accepts malicious data.
// β οΈ VULNERABLE
for acc in ctx.remaining_accounts {
let data = acc.try_borrow_data()?; // No validation!
}
// β
SECURE
for acc in ctx.remaining_accounts {
require!(acc.owner == ctx.program_id, InvalidOwner);
let data = MyType::try_deserialize(&mut &acc.data.borrow()[..])?;
}Problem: Making CPI calls without verifying the target program.
// β οΈ VULNERABLE
invoke(&ix, &[acc])?; // external_program could be malicious!
// β
SECURE
require!(external_program.key() == EXPECTED_PROGRAM, InvalidProgram);
invoke(&ix, &[acc])?;Problem: Manual deserialization without checking the 8-byte discriminator.
// β οΈ VULNERABLE
let data = account.try_borrow_data()?;
let vault = parse_vault(&data[8..]); // Skips discriminator!
// β
SECURE
pub vault: Account<'info, Vault>, // Anchor checks discriminatorProblem: Using as for type conversion silently truncates values.
// β οΈ VULNERABLE
let small: u32 = large_u64 as u32; // Truncates silently!
// β
SECURE
let small: u32 = u32::try_from(large_u64).map_err(|_| ErrorCode::Overflow)?;Problem: Using init allows front-running to block account creation.
// β οΈ VULNERABLE
#[account(init, ...)] // Fails if account exists!
pub user_account: Account<'info, UserData>,
// β
SECURE
#[account(init_if_needed, ...)] // Works either way
pub user_account: Account<'info, UserData>,Problem: Checking state before a change, then using stale check results.
// β οΈ VULNERABLE
let is_valid = check_ratio(balance); // T1: check
do_something_that_changes_state();
if is_valid { withdraw(); } // T2: use stale result!
// β
SECURE
let new_balance = balance - amount;
require!(check_ratio(new_balance), Invalid); // Check final state
account.balance = new_balance;Problem: Withdrawing lamports below rent-exempt threshold purges the account.
// β οΈ VULNERABLE
**account.lamports.borrow_mut()? -= amount; // May go below rent!
// β
SECURE
let rent_min = Rent::get()?.minimum_balance(account.data_len());
require!(account.lamports() - amount >= rent_min, RentViolation);Each example is a standalone Anchor project. To build:
# Build a specific example
cd 0-missing-signer-check && anchor build
# Or build all
for dir in */; do
if [ -f "$dir/Anchor.toml" ]; then
echo "Building $dir..."
(cd "$dir" && anchor build)
fi
done- Rust (1.70+)
- Solana CLI (1.17+)
- Anchor (0.32+)
anchor-bounty/
βββ 0-missing-signer-check/
β βββ programs/
β β βββ missing_signer_check/src/
β β βββ instructions/
β β β βββ withdraw_vulnerable.rs
β β β βββ withdraw_secure.rs
β β βββ lib.rs
β βββ README.md
βββ 1-arithmetic-overflow/
β βββ ...
βββ 2-account-revival/
β βββ ...
...
βββ README.md (this file)
- β
Use
Signer<'info>for authorization - β
Use
Account<'info, T>for type-safe deserialization - β
Use
checked_*methods for arithmetic - β
Use Anchor's
closeconstraint to close accounts - β
Validate all accounts in
remaining_accounts - β Verify CPI target programs
- β
Use
init_if_neededfor user-facing operations - β Check rent exemption before withdrawing lamports
- β Verify invariants on final state (not cached values)
- β Use
AccountInfowhenSignerorAccount<T>is appropriate - β Use raw
-,+,*operators on untrusted values - β Manually zero lamports to close accounts
- β Skip discriminator checks on manual deserialization
- β Use
asfor potentially lossy type conversions - β Trust cached values after state-changing operations
- β Allow withdrawals that violate rent exemption
Contributions are welcome! If you'd like to add more vulnerability examples:
- Create a new numbered folder (e.g.,
11-new-vulnerability/) - Include vulnerable and secure implementations
- Write a comprehensive README
- Submit a pull request
MIT
These examples are for educational purposes only. The vulnerable code intentionally contains security flaws to demonstrate common mistakes. Never use vulnerable patterns in production code.
Built with Anchor | Learn more about Solana Security