Scans Solana programs for 6 critical vulnerabilities including arbitrary CPI, improper PDA validation, missing signer/ownership checks, and sysvar spoofing. Use when auditing Solana/Anchor programs.
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.
.rs// 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, bump
programs/*/src/lib.rs - Program implementationAnchor.toml - Anchor configurationCargo.toml with solana-program or anchor-langtests/ - Program testsWhen invoked, I will:
I check for 6 critical vulnerability patterns unique to Solana. For detailed detection patterns, code examples, mitigations, and testing strategies, see VULNERABILITY_PATTERNS.md.
For complete vulnerability patterns with code examples, see VULNERABILITY_PATTERNS.md.
programs/*/src/lib.rs)# Find all CPI calls
rg "invoke\(|invoke_signed\(" programs/
# Check for program ID validation before each
# Should see program ID checks immediately before invoke
For each CPI:
Program<'info, T> type# 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:
find_program_address() or Anchor seeds constraint# 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:
Account<'info, T> and Signer<'info># 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/
# 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)
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 — cite file:line and 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 not n/a. Pattern 5 is version-scoped (pre-Solana 1.8.1): cite the solana-program
version 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.
## [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`
#[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
}
}
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
}
});
});
# Run local validator for testing
solana-test-validator
# Deploy and test program
anchor test
building-secure-contracts/not-so-smart-contracts/solana/Before completing Solana program audit:
CPI Security (CRITICAL):
invoke()Program<'info, T> typePDA Security (CRITICAL):
find_program_address() or Anchor seeds constraintAccount Validation (HIGH):
account.owner == expected_program_idAccount<'info, T> typeSigner Validation (CRITICAL):
is_signeraccount.is_signer == trueSigner<'info> typeSysvar Security (HIGH):
load_instruction_at_checked()Instruction Introspection (MEDIUM):
Testing:
found, clear or n/a with a reasonn/a costs one
clause and makes the judgment reviewable. Silence records nothing, and a reader cannot tell it apart from
not having checked.#[account(mut)] is not an ownership check, and UncheckedAccount opts out
entirely. Cite the attribute, not the framework.create_program_address without the
canonical bump admits multiple valid addresses, which is pattern 2 in full.npx skills add trailofbits/solana-vulnerability-scanner下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Notion API for creating and managing pages, databases, and blocks.
Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output.
Best practices for using the oracle CLI (prompt + file bundling, engines, sessions, and file attachment patterns).
Transcribe audio via OpenAI Audio Transcriptions API (Whisper).
Query Google Places API (New) via the goplaces CLI for text search, place details, resolve, and reviews. Use for human-friendly place lookup or JSON output for scripts.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer