Skip to content

codewithmide/Anchor-Security-Practices

Repository files navigation

Anchor Security Examples

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.

Overview

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

Vulnerabilities Covered

# 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

Quick Reference

0. Missing Signer Check

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 signature

1. Arithmetic Overflow/Underflow

Problem: Using -, +, * operators can silently overflow/underflow.

// ⚠️ VULNERABLE
balance = balance - amount;  // Wraps on underflow!

// βœ… SECURE
balance = balance.checked_sub(amount).ok_or(ErrorCode::Underflow)?;

2. Account Revival

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 everything

3. Missing Owner Check

Problem: 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 owner

4. Remaining Accounts Validation

Problem: 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()[..])?;
}

5. Unverified CPI Target

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])?;

6. Missing Discriminator Check

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 discriminator

7. Type Casting Issues

Problem: 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)?;

8. Initialization DoS

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>,

9. TOCTOU (Time-of-Check to Time-of-Use)

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;

10. Missing Rent Exemption Check

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);

Building All Examples

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

Prerequisites

Project Structure

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)

Security Best Practices Summary

Always Do

  • βœ… Use Signer<'info> for authorization
  • βœ… Use Account<'info, T> for type-safe deserialization
  • βœ… Use checked_* methods for arithmetic
  • βœ… Use Anchor's close constraint to close accounts
  • βœ… Validate all accounts in remaining_accounts
  • βœ… Verify CPI target programs
  • βœ… Use init_if_needed for user-facing operations
  • βœ… Check rent exemption before withdrawing lamports
  • βœ… Verify invariants on final state (not cached values)

Never Do

  • ❌ Use AccountInfo when Signer or Account<T> is appropriate
  • ❌ Use raw -, +, * operators on untrusted values
  • ❌ Manually zero lamports to close accounts
  • ❌ Skip discriminator checks on manual deserialization
  • ❌ Use as for potentially lossy type conversions
  • ❌ Trust cached values after state-changing operations
  • ❌ Allow withdrawals that violate rent exemption

Contributing

Contributions are welcome! If you'd like to add more vulnerability examples:

  1. Create a new numbered folder (e.g., 11-new-vulnerability/)
  2. Include vulnerable and secure implementations
  3. Write a comprehensive README
  4. Submit a pull request

License

MIT

Disclaimer

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published