Solana Vulnerability Scanner
Trail of BitsOfficialPublished by Trail of Bits
- Skill
- Review and audit
- Security
- CC-BY-SA-4.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The Solana Vulnerability Scanner provides a structured framework for auditing Solana smart contracts. It focuses on six critical security patterns, including cross-program invocation (CPI) security, PDA validation, ownership checks, signer verification, sysvar integrity, and instruction introspection. The skill mandates a comprehensive reporting format that requires a coverage table for all six patterns, ensuring that every aspect of the program's security posture is explicitly evaluated, documented, and verified.
What it does
- Scans native Rust and Anchor-based Solana programs for 6 critical security vulnerabilities.
- Enforces a mandatory coverage table to ensure all security patterns are addressed.
- Provides specific remediation strategies and code examples for identified vulnerabilities.
- Guides users through a multi-step audit workflow covering CPI, PDA, and account validation.
- Integrates with standard Solana testing practices and linting tools.
- Requires explicit justification for any pattern marked as not applicable.
When to use it
- During the pre-launch security assessment of a Solana protocol.
- When reviewing cross-program invocation logic for potential security flaws.
- When validating program-derived address (PDA) implementations.
- During routine audits of Solana or Anchor program codebases.
- When verifying that account validation patterns are correctly implemented.
Advantages
- Prevents incomplete audits by requiring a full coverage table for all 6 patterns.
- Standardizes the reporting of security findings across different projects.
- Provides actionable, code-level recommendations for fixing identified issues.
- Reduces human error by enforcing a structured, step-by-step auditing workflow.
- Differentiates between 'clear' code and 'n/a' scenarios to ensure thoroughness.
Usage examples
- “Audit my Solana program for security vulnerabilities.”
- The agent will scan the codebase, generate a 6-row coverage table, and provide detailed findings or 'clear' status for each of the 6 critical vulnerability patterns.
- “Check my Anchor program's PDA implementation for security issues.”
- The agent will review the PDA derivation logic, verify the use of canonical bumps, and report whether the implementation is secure or requires remediation.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/trailofbits/solana-vulnerability-scanner/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip solana-vulnerability-scanner.zip -d .agents/skills
Documentation by Trail of Bits
Solana Vulnerability Scanner
1. Purpose
Systematically scan Solana programs (native and Anchor framework) for platform-specific security vulnerabilities related to cross-program invocations, account validation, and program-derived addresses. This skill encodes 6 critical vulnerability patterns unique to Solana's account model.
2. When to Use This Skill
Auditing Solana programs (native Rust or Anchor)
Reviewing cross-program invocation (CPI) logic
Validating program-derived address (PDA) implementations
Pre-launch security assessment of Solana protocols
Reviewing account validation patterns
Assessing instruction introspection logic
3. Platform Detection
File Extensions & Indicators
Rust files:
.rs
Language/Framework Markers
// Native Solana program indicators
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
program::invoke,
program::invoke_signed,
};
entrypoint!(process_instruction);
// Anchor framework indicators
use anchor_lang::prelude::*;
#[program]
pub mod my_program {
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Program logic
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub authority: Signer<'info>,
}
// Common patterns
AccountInfo, Pubkey
invoke(), invoke_signed()
Signer<'info>, Account<'info>
#[account(...)] with constraints
seeds, bumpProject Structure
programs/*/src/lib.rs- Program implementationAnchor.toml- Anchor configurationCargo.tomlwithsolana-programoranchor-langtests/- Program tests
Tool Support
Trail of Bits Solana Lints: Rust linters for Solana
Installation: Add to Cargo.toml
anchor test: Built-in testing framework
Solana Test Validator: Local testing environment
4. How This Skill Works
When invoked, I will:
Search your codebase for Solana/Anchor programs
Analyze each program for the 6 vulnerability patterns
Report findings with file references and severity, above them a coverage table carrying a verdict for every pattern
Provide fixes for each identified issue
Check account validation and CPI security
5. Example Output
6. Vulnerability Patterns (6 Patterns)
I check for 6 critical vulnerability patterns unique to Solana. For detailed detection patterns, code examples, mitigations, and testing strategies, see VULNERABILITY_PATTERNS.md.
Pattern Summary:
Arbitrary CPI ⚠️ CRITICAL - User-controlled program IDs in CPI calls
Improper PDA Validation ⚠️ CRITICAL - Using create_program_address without canonical bump
Missing Ownership Check ⚠️ HIGH - Deserializing accounts without owner validation
Missing Signer Check ⚠️ CRITICAL - Authority operations without is_signer check
Sysvar Account Check ⚠️ HIGH - Spoofed sysvar accounts (pre-Solana 1.8.1)
Improper Instruction Introspection ⚠️ MEDIUM - Absolute indexes allowing reuse
For complete vulnerability patterns with code examples, see VULNERABILITY_PATTERNS.md.
7. Scanning Workflow
Step 1: Platform Identification
Verify Solana program (native or Anchor)
Check Solana version (1.8.1+ for sysvar security)
Locate program source (
programs/*/src/lib.rs)Identify framework (native vs Anchor)
Step 2: CPI Security Review
# Find all CPI calls
rg "invoke\(|invoke_signed\(" programs/
# Check for program ID validation before each
# Should see program ID checks immediately before invokeFor each CPI:
[ ] Program ID validated before invocation
[ ] Cannot pass user-controlled program accounts
[ ] Anchor: Uses
Program<'info, T>type
Step 3: PDA Validation Check
# Find PDA usage
rg "find_program_address|create_program_address" programs/
rg "seeds.*bump" programs/
# Anchor: Check for seeds constraints
rg "#\[account.*seeds" programs/For each PDA:
[ ] Uses
find_program_address()or Anchorseedsconstraint[ ] Bump seed stored and reused
[ ] Not using user-provided bump
Step 4: Account Validation Sweep
# Find account deserialization
rg "try_from_slice|try_deserialize" programs/
# Should see owner checks before deserialization
rg "\.owner\s*==|\.owner\s*!=" programs/For each account used:
[ ] Owner validated before deserialization
[ ] Signer check for authority accounts
[ ] Anchor: Uses
Account<'info, T>andSigner<'info>
Step 5: Instruction Introspection Review
# Find instruction introspection usage
rg "load_instruction_at|load_current_index|get_instruction_relative" programs/
# Check for checked versions
rg "load_instruction_at_checked|load_current_index_checked" programs/[ ] Using checked functions (Solana 1.8.1+)
[ ] Using relative indexing
[ ] Proper correlation validation
Step 6: Trail of Bits Solana Lints
# Add to Cargo.toml
[dependencies]
solana-program = "1.17" # Use latest version
[lints.clippy]
# Enable Solana-specific lints
# (Trail of Bits solana-lints if available)8. Reporting Format
Coverage Table
Report on every pattern in §6, whether or not it turned anything up. Emit this table above the findings, with all 6 rows present:
| # | Pattern | Verdict | Evidence |
|---|---|---|---|
| 1 | Arbitrary CPI | n/a | this program makes no cross-program invocations |
| 2 | Improper PDA Validation | ||
| 3 | Missing Ownership Check | ||
| 4 | Missing Signer Check | ||
| 5 | Sysvar Account Check | ||
| 6 | Improper Instruction Introspection |
Each verdict is one of:
found— citefile:lineand write the finding up in full below.clear— the pattern applies to this program and the program handles it. Name the constraint, account type, or check you searched for, so a reader can repeat the search.n/a— the pattern cannot apply here. Give the reason in one clause ("this program makes no CPI calls"). Not having looked is notn/a. Pattern 5 is version-scoped (pre-Solana 1.8.1): cite thesolana-programversion the program targets rather than dropping the row, since "targets 1.17, fixed upstream" and "did not look" are otherwise the same answer.
A table with fewer than 6 rows is an incomplete scan and must be reported as one. A row whose Verdict cell is empty is incomplete in the same way: row 1 above is filled in to show the shape, and every row is filled in the same way before the report is done. Six clear verdicts is a
result a reader can act on. A report that covers two patterns and says nothing about the other four reads
exactly like a clean program, and that is the failure this table exists to prevent.
Finding Template
## [CRITICAL] Arbitrary CPI - Unchecked Program ID
**Location**: `programs/vault/src/lib.rs:145-160` (withdraw function)
**Description**:
The `withdraw` function performs a CPI to transfer SPL tokens without validating that the provided `token_program` account is actually the SPL Token program. An attacker can provide a malicious program that appears to perform a transfer but actually steals tokens or performs unauthorized actions.
**Vulnerable Code**:
```rust
// lib.rs, line 145
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let token_program = &ctx.accounts.token_program;
// WRONG: No validation of token_program.key()!
invoke(
&spl_token::instruction::transfer(...),
&[
ctx.accounts.vault.to_account_info(),
ctx.accounts.destination.to_account_info(),
ctx.accounts.authority.to_account_info(),
token_program.to_account_info(), // UNVALIDATED
],
)?;
Ok(())
}
```
**Attack Scenario**:
1. Attacker deploys malicious "token program" that logs transfer instruction but doesn't execute it
2. Attacker calls withdraw() providing malicious program as token_program
3. Vault's authority signs the transaction
4. Malicious program receives CPI with vault's signature
5. Malicious program can now impersonate vault and drain real tokens
**Recommendation**:
Use Anchor's `Program<'info, Token>` type:
```rust
use anchor_spl::token::{Token, Transfer};
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
#[account(mut)]
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>, // Validates program ID automatically
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
anchor_spl::token::transfer(cpi_ctx, amount)?;
Ok(())
}
```
**References**:
- building-secure-contracts/not-so-smart-contracts/solana/arbitrary_cpi
- Trail of Bits lint: `unchecked-cpi-program-id`9. Priority Guidelines
Critical (Immediate Fix Required)
Arbitrary CPI (attacker-controlled program execution)
Improper PDA validation (account spoofing)
Missing signer check (unauthorized access)
High (Fix Before Launch)
Missing ownership check (fake account data)
Sysvar account check (authentication bypass, pre-1.8.1)
Medium (Address in Audit)
Improper instruction introspection (logic bypass)
10. Testing Recommendations
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn test_rejects_wrong_program_id() {
// Provide wrong program ID, should fail
}
#[test]
#[should_panic]
fn test_rejects_non_canonical_pda() {
// Provide non-canonical bump, should fail
}
#[test]
#[should_panic]
fn test_requires_signer() {
// Call without signature, should fail
}
}Integration Tests (Anchor)
import * as anchor from "@coral-xyz/anchor";
describe("security tests", () => {
it("rejects arbitrary CPI", async () => {
const fakeTokenProgram = anchor.web3.Keypair.generate();
try {
await program.methods
.withdraw(amount)
.accounts({
tokenProgram: fakeTokenProgram.publicKey, // Wrong program
})
.rpc();
assert.fail("Should have rejected fake program");
} catch (err) {
// Expected to fail
}
});
});Solana Test Validator
# Run local validator for testing
solana-test-validator
# Deploy and test program
anchor test11. Additional Resources
Building Secure Contracts:
building-secure-contracts/not-so-smart-contracts/solana/Trail of Bits Solana Lints: https://github.com/trailofbits/solana-lints
Anchor Documentation: https://www.anchor-lang.com/
Solana Program Library: https://github.com/solana-labs/solana-program-library
Solana Cookbook: https://solanacookbook.com/
12. Quick Reference Checklist
Before completing Solana program audit:
CPI Security (CRITICAL):
[ ] ALL CPI calls validate program ID before
invoke()[ ] Cannot use user-provided program accounts
[ ] Anchor: Uses
Program<'info, T>type
PDA Security (CRITICAL):
[ ] PDAs use
find_program_address()or Anchorseedsconstraint[ ] Bump seed stored and reused (not user-provided)
[ ] PDA accounts validated against canonical address
Account Validation (HIGH):
[ ] ALL accounts check owner before deserialization
[ ] Native: Validates
account.owner == expected_program_id[ ] Anchor: Uses
Account<'info, T>type
Signer Validation (CRITICAL):
[ ] ALL authority accounts check
is_signer[ ] Native: Validates
account.is_signer == true[ ] Anchor: Uses
Signer<'info>type
Sysvar Security (HIGH):
[ ] Using Solana 1.8.1+
[ ] Using checked functions:
load_instruction_at_checked()[ ] Sysvar addresses validated
Instruction Introspection (MEDIUM):
[ ] Using relative indexes for correlation
[ ] Proper validation between related instructions
[ ] Cannot reuse same instruction across multiple calls
Testing:
[ ] Unit tests cover all account validation
[ ] Integration tests with malicious inputs
[ ] Local validator testing completed
[ ] Trail of Bits lints enabled and passing
[ ] Coverage table emitted with all 6 rows, each carrying a verdict of
found,clearorn/awith a reason
13. Rationalizations to Reject
"The program is small, so most patterns obviously don't apply." Obvious to whom? An
n/acosts one clause and makes the judgment reviewable. Silence records nothing, and a reader cannot tell it apart from not having checked."The Trail of Bits lints pass, so the program is clean." The lints cover a subset of these 6 patterns. A clean lint run is one row of evidence, not a verdict on the patterns it never examined. Say which patterns it covered.
"I checked the patterns that matter for this program." Deciding which patterns matter is the scan, not a precondition for starting it. Rank by severity after the table is complete, not by leaving rows out.
"No findings, so there is nothing to report." A zero-finding scan still emits the full coverage table. That table is the deliverable: it is what distinguishes a program that was examined from one that was glanced at.
"Anchor handles account validation." Name the constraint. Anchor validates what the account struct declares and nothing more:
#[account(mut)]is not an ownership check, andUncheckedAccountopts out entirely. Cite the attribute, not the framework."The PDA derivation makes it safe." Derivation is not validation.
create_program_addresswithout the canonical bump admits multiple valid addresses, which is pattern 2 in full.
Files
- ATTRIBUTION.mdAdded by AutoGineer975 B
- LICENSELicence file added by AutoGineer19.7 KiB
- SKILL.md14.1 KiB
- agents/openai.yaml258 B
- assets/trail-of-bits-mark.svg3.0 KiB
- resources/VULNERABILITY_PATTERNS.md19.9 KiB
resources/VULNERABILITY_PATTERNS.md
6.1 ARBITRARY CPI (Cross-Program Invocation) ⚠️ CRITICAL
Description: Using invoke() or invoke_signed() with user-controlled program IDs allows attackers to call malicious programs instead of the intended program.
Detection Patterns:
// VULNERABLE: User-provided program ID without validation
pub fn transfer_tokens(
ctx: Context<TransferTokens>,
amount: u64,
) -> Result<()> {
// User provides token_program account
let token_program = &ctx.accounts.token_program;
// WRONG: No check that token_program.key() == spl_token::ID!
invoke(
&spl_token::instruction::transfer(...),
&[
ctx.accounts.from.to_account_info(),
ctx.accounts.to.to_account_info(),
token_program.to_account_info(), // ATTACKER CONTROLLED!
],
)?;
Ok(())
}
// VULNERABLE: Native Solana without validation
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let token_program = next_account_info(accounts_iter)?;
// WRONG: No validation of token_program.key
invoke(
&transfer_instruction,
&[from_account, to_account, token_program], // Unvalidated!
)?;
Ok(())
}What to Check:
[ ] ALL CPI program IDs validated before
invoke()orinvoke_signed()[ ] Validation:
program.key() == EXPECTED_PROGRAM_ID[ ] Cannot pass arbitrary program accounts from user
[ ] Anchor: Use
Program<'info, T>type with constraint
Mitigation:
// SECURE: Validate program ID (Native)
use spl_token;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let token_program = next_account_info(accounts_iter)?;
// CRITICAL: Validate program ID
if token_program.key != &spl_token::ID {
return Err(ProgramError::IncorrectProgramId);
}
// Safe to invoke
invoke(
&spl_token::instruction::transfer(...),
&[from_account, to_account, token_program],
)?;
Ok(())
}
// SECURE: Use Anchor Program type with constraint
use anchor_spl::token::{Token, TokenAccount};
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
// Program<'info, Token> automatically validates program ID
pub token_program: Program<'info, Token>,
}
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
// Anchor ensures token_program.key() == Token::id()
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
anchor_spl::token::transfer(cpi_ctx, amount)?;
Ok(())
}Tool Detection:
Trail of Bits lint:
unchecked-cpi-program-idLook for:
invoke()without prior program ID check
References: building-secure-contracts/not-so-smart-contracts/solana/arbitrary_cpi
4.2 IMPROPER PDA VALIDATION ⚠️ CRITICAL
Description: Program-Derived Addresses (PDAs) can have multiple valid bumps for the same seeds. Using create_program_address() without verifying canonical bump allows PDA spoofing attacks.
Detection Patterns:
// VULNERABLE: Using create_program_address without bump validation
pub fn withdraw(ctx: Context<Withdraw>, bump: u8) -> Result<()> {
// User provides bump
let vault_seeds = &[
b"vault",
ctx.accounts.user.key().as_ref(),
&[bump], // WRONG: Attacker can provide non-canonical bump!
];
let vault = Pubkey::create_program_address(vault_seeds, ctx.program_id)?;
// This vault might not be the canonical PDA!
// Attacker could create multiple PDAs and drain wrong vault
Ok(())
}
// VULNERABLE: Not comparing with find_program_address result
pub fn initialize(ctx: Context<Initialize>, bump: u8) -> Result<()> {
let pda_seeds = &[b"state", &[bump]];
let pda = Pubkey::create_program_address(pda_seeds, ctx.program_id)?;
// WRONG: Not verifying this is the canonical PDA
// Should check pda == ctx.accounts.pda_account.key()
}What to Check:
[ ] PDAs use
find_program_address()to get canonical bump[ ] OR
create_program_address()result compared with expected PDA[ ] Bump seed stored and reused (not provided by user)
[ ] Anchor: Use
seedsandbumpconstraints
Mitigation:
// SECURE: Use find_program_address (Native)
pub fn withdraw(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let vault_account = next_account_info(accounts_iter)?;
let user_account = next_account_info(accounts_iter)?;
// Find canonical PDA with bump
let (vault_pda, bump) = Pubkey::find_program_address(
&[b"vault", user_account.key.as_ref()],
program_id,
);
// Verify provided account matches canonical PDA
if vault_account.key != &vault_pda {
return Err(ProgramError::InvalidAccountData);
}
// Use bump for signing
let vault_seeds = &[
b"vault",
user_account.key.as_ref(),
&[bump],
];
invoke_signed(
&transfer_instruction,
&[vault_account, destination],
&[vault_seeds],
)?;
Ok(())
}
// SECURE: Anchor with seeds constraint
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(
mut,
seeds = [b"vault", user.key().as_ref()],
bump, // Anchor automatically validates canonical bump
)]
pub vault: Account<'info, VaultAccount>,
pub user: Signer<'info>,
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// Anchor has already validated vault is canonical PDA
let bump = *ctx.bumps.get("vault").unwrap();
let vault_seeds = &[
b"vault",
ctx.accounts.user.key().as_ref(),
&[bump],
];
// Safe to use in CPI
let signer_seeds = &[&vault_seeds[..]];
// CPI with PDA signer
Ok(())
}
// BETTER: Store bump in account
#[account]
pub struct VaultAccount {
pub bump: u8, // Store canonical bump
pub owner: Pubkey,
pub balance: u64,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = user,
space = 8 + 1 + 32 + 8,
seeds = [b"vault", user.key().as_ref()],
bump,
)]
pub vault: Account<'info, VaultAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.bump = *ctx.bumps.get("vault").unwrap(); // Store canonical bump
vault.owner = ctx.accounts.user.key();
vault.balance = 0;
Ok(())
}Tool Detection:
Trail of Bits lint:
improper-pda-validationLook for:
create_program_addresswithoutfind_program_addresscomparison
References: building-secure-contracts/not-so-smart-contracts/solana/pda_validation
4.3 MISSING OWNERSHIP CHECK ⚠️ HIGH
Description: Accounts without owner validation can be spoofed by attackers. User provides account with attacker-controlled data, bypassing program logic.
Detection Patterns:
// VULNERABLE: Deserializing account without owner check
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let vault_account = next_account_info(accounts_iter)?;
// WRONG: No owner check before deserializing!
let vault: Vault = Vault::try_from_slice(&vault_account.data.borrow())?;
// vault could be fake account owned by attacker with fake balance!
if vault.balance >= amount {
// Process withdrawal using fake balance
}
Ok(())
}
// VULNERABLE: Anchor without owner constraint
#[derive(Accounts)]
pub struct Withdraw<'info> {
/// CHECK: This is unsafe - no owner validation!
pub vault: AccountInfo<'info>,
}What to Check:
[ ] ALL accounts validated for correct owner before deserialization
[ ] Native: Check
account.owner == expected_program_id[ ] Anchor: Use
Account<'info, T>type (automatic owner check)[ ] System accounts: Check
account.owner == system_program::ID[ ] Token accounts: Check
account.owner == spl_token::ID
Mitigation:
// SECURE: Validate owner before deserializing (Native)
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let vault_account = next_account_info(accounts_iter)?;
// CRITICAL: Validate owner
if vault_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Safe to deserialize - we own this account
let vault: Vault = Vault::try_from_slice(&vault_account.data.borrow())?;
Ok(())
}
// SECURE: Use Anchor Account type (automatic validation)
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub vault: Account<'info, VaultAccount>, // Anchor checks owner automatically
pub user: Signer<'info>,
}
// For third-party program accounts
#[derive(Accounts)]
pub struct ProcessToken<'info> {
#[account(mut)]
pub token_account: Account<'info, TokenAccount>, // Validates owner == Token program
pub token_program: Program<'info, Token>,
}Tool Detection:
Trail of Bits lint:
missing-ownership-checkLook for: Deserialization without owner validation
References: building-secure-contracts/not-so-smart-contracts/solana/ownership_check
4.4 MISSING SIGNER CHECK ⚠️ CRITICAL
Description: Sensitive operations without is_signer validation allow unauthorized users to call functions intended for specific authorities.
Detection Patterns:
// VULNERABLE: No signer check on authority account
pub fn withdraw(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let vault = next_account_info(accounts_iter)?;
let authority = next_account_info(accounts_iter)?;
// WRONG: No check that authority.is_signer == true!
// Attacker can provide any authority account and withdraw
let vault_data: Vault = Vault::try_from_slice(&vault.data.borrow())?;
// Check authority matches (but attacker provided this!)
if vault_data.authority != *authority.key {
return Err(ProgramError::InvalidAccountData);
}
// Process withdrawal - ATTACKER CAN CALL THIS!
Ok(())
}
// VULNERABLE: Anchor without Signer type
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub vault: Account<'info, VaultAccount>,
/// CHECK: Missing signer constraint!
pub authority: AccountInfo<'info>,
}What to Check:
[ ] ALL authority accounts validated with
is_signer[ ] Native: Check
account.is_signer == true[ ] Anchor: Use
Signer<'info>type (automatic validation)[ ] Access-controlled functions require signer check
Mitigation:
// SECURE: Check is_signer (Native)
pub fn withdraw(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let vault = next_account_info(accounts_iter)?;
let authority = next_account_info(accounts_iter)?;
// CRITICAL: Verify authority signed the transaction
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let vault_data: Vault = Vault::try_from_slice(&vault.data.borrow())?;
// Now safe to check authority matches
if vault_data.authority != *authority.key {
return Err(ProgramError::InvalidAccountData);
}
// Process withdrawal - only if authority signed
Ok(())
}
// SECURE: Use Anchor Signer type (automatic validation)
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(
mut,
has_one = authority, // Also validate vault.authority == authority.key()
)]
pub vault: Account<'info, VaultAccount>,
pub authority: Signer<'info>, // Anchor checks is_signer automatically
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// Anchor has already validated:
// 1. authority.is_signer == true
// 2. vault.authority == authority.key()
// Safe to proceed with withdrawal
Ok(())
}
// For admin functions
#[derive(Accounts)]
pub struct UpdateConfig<'info> {
#[account(
mut,
has_one = admin,
)]
pub config: Account<'info, Config>,
pub admin: Signer<'info>, // Must be signer
}Tool Detection:
Trail of Bits lint:
missing-signer-checkLook for: Authority checks without
is_signervalidation
References: building-secure-contracts/not-so-smart-contracts/solana/signer_check
4.5 SYSVAR ACCOUNT CHECK ⚠️ HIGH (Pre-Solana 1.8.1)
Description: In Solana versions before 1.8.1, users can pass spoofed sysvar accounts (Instructions, Clock, etc.) to bypass authentication. This affects load_instruction_at() and similar functions.
Detection Patterns:
// VULNERABLE: Using unchecked load functions (Solana < 1.8.1)
use solana_program::sysvar::instructions;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let instructions_sysvar = next_account_info(accounts_iter)?;
// WRONG: load_instruction_at() doesn't validate sysvar account!
let current_ix = instructions::load_instruction_at(0, instructions_sysvar)?;
// Attacker can provide fake Instructions sysvar with spoofed instruction data!
// Bypass authentication by faking previous instruction
}
// VULNERABLE: load_current_index() without validation
let current_index = instructions::load_current_index(instructions_sysvar)?;What to Check:
[ ] Using Solana 1.8.1 or higher
[ ] Using checked functions:
load_instruction_at_checked(),load_current_index_checked()[ ] NOT using:
load_instruction_at(),load_current_index()(unchecked versions)[ ] Sysvar accounts validated against known addresses
Mitigation:
// OPTION 1: Upgrade to Solana 1.8.1+ and use checked functions
use solana_program::sysvar::instructions;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let instructions_sysvar = next_account_info(accounts_iter)?;
// SECURE: load_instruction_at_checked validates sysvar account
let current_ix = instructions::load_instruction_at_checked(
0,
instructions_sysvar
)?;
// Safe - sysvar is validated
Ok(())
}
// OPTION 2: Manual validation (if on old Solana version)
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let instructions_sysvar = next_account_info(accounts_iter)?;
// Validate sysvar account address
if instructions_sysvar.key != &solana_program::sysvar::instructions::ID {
return Err(ProgramError::InvalidAccountData);
}
// Now safe to use unchecked function
let current_ix = instructions::load_instruction_at(0, instructions_sysvar)?;
Ok(())
}
// SECURE: Anchor with address constraint
#[derive(Accounts)]
pub struct CheckInstructions<'info> {
/// CHECK: Validated against known sysvar address
#[account(address = solana_program::sysvar::instructions::ID)]
pub instructions_sysvar: AccountInfo<'info>,
}Tool Detection:
Trail of Bits lint:
unchecked-sysvar-accountLook for:
load_instruction_at()instead ofload_instruction_at_checked()
References: building-secure-contracts/not-so-smart-contracts/solana/sysvar_get
4.6 IMPROPER INSTRUCTION INTROSPECTION ⚠️ MEDIUM
Description: Using absolute indexes in instruction introspection allows reusing the same instruction context across multiple program calls. Should use relative indexes to ensure proper correlation.
Detection Patterns:
// VULNERABLE: Absolute index in load_instruction_at
use solana_program::sysvar::instructions;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instructions_sysvar = &accounts[0];
// WRONG: Using absolute index 0
let prev_ix = instructions::load_instruction_at_checked(0, instructions_sysvar)?;
// Attacker can craft transaction where instruction 0 is benign,
// but instruction 1 (malicious) also loads instruction 0 for validation
// Same instruction 0 used to validate both instruction 0 and 1!
}
// VULNERABLE: No correlation between instructions
pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> {
let instructions_sysvar = &ctx.accounts.instructions_sysvar;
// Check that previous instruction was deposit
let prev_ix = instructions::load_instruction_at_checked(0, instructions_sysvar)?;
// WRONG: Not checking that prev_ix is actually related to current instruction
// Could be completely unrelated instruction from earlier in transaction
}What to Check:
[ ] Use relative indexes:
get_instruction_relative(-1, ...)for previous instruction[ ] Absolute indexes only when specifically intended
[ ] Validate correlation between current and referenced instructions
[ ] Cannot reuse same instruction validation across multiple calls
Mitigation:
// SECURE: Use relative indexing
use solana_program::sysvar::instructions;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instructions_sysvar = &accounts[0];
// Get current instruction index
let current_index = instructions::load_current_index_checked(instructions_sysvar)?;
// SECURE: Get immediately preceding instruction with relative index
if current_index > 0 {
let prev_ix = instructions::load_instruction_at_checked(
(current_index - 1) as usize,
instructions_sysvar
)?;
// This is guaranteed to be the instruction immediately before current
// Validate prev_ix is the expected setup instruction
}
Ok(())
}
// BETTER: Use get_instruction_relative (if available)
let prev_ix = instructions::get_instruction_relative(-1, instructions_sysvar)?;
// Explicitly relative to current instruction
// SECURE: Additional correlation validation
pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> {
let instructions_sysvar = &ctx.accounts.instructions_sysvar;
let current_ix_index = instructions::load_current_index_checked(instructions_sysvar)?;
// Must have previous instruction
require!(current_ix_index > 0, ErrorCode::NoPreviousInstruction);
// Get previous instruction
let prev_ix_index = current_ix_index - 1;
let prev_ix = instructions::load_instruction_at_checked(
prev_ix_index as usize,
instructions_sysvar
)?;
// Validate previous instruction is deposit to same program
require!(
prev_ix.program_id == ctx.program_id,
ErrorCode::InvalidPreviousProgram
);
// Validate accounts in previous instruction match expectations
// This ensures proper correlation between deposit and withdraw
require!(
prev_ix.accounts[0].pubkey == ctx.accounts.vault.key(),
ErrorCode::VaultMismatch
);
Ok(())
}References: building-secure-contracts/not-so-smart-contracts/solana/insecure_instruction_introspection
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Trail of Bits
- Publisher
- Trail of Bits (Official)
- Source
- trailofbits/skills · plugins/building-secure-contracts/skills/solana-vulnerability-scanner · commit e4dd0bb
- Licence
- Creative Commons Attribution-ShareAlike 4.0 International (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Trail of Bits.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download solana-vulnerability-scanner.zip