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.
Install
Use withClaudeCursorCodex
The file stays in the publisher's repository. PlugMCP does not host or rewrite it.
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.
name: solana-vulnerability-scanner
description: 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.
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.
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, bump
Project Structure
programs/*/src/lib.rs - Program implementation
Anchor.toml - Anchor configuration
Cargo.toml with solana-program or anchor-lang
tests/ - 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
# Find all CPI calls
rg "invoke\(|invoke_signed\(" programs/
# Check for program ID validation before each
# Should see program ID checks immediately before invoke
# 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 — 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.
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)
#[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
}
}
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, clear or n/a with a reason
13. Rationalizations to Reject
"The program is small, so most patterns obviously don't apply." Obvious to whom? An n/a costs 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, and UncheckedAccount opts out
entirely. Cite the attribute, not the framework.
"The PDA derivation makes it safe." Derivation is not validation. create_program_address without the
canonical bump admits multiple valid addresses, which is pattern 2 in full.
How do I install solana-vulnerability-scanner?
Download plugins/building-secure-contracts/skills/solana-vulnerability-scanner/SKILL.md from the publisher, or copy the path and open the repository. PlugMCP lists the skill and does not host the file.
Where does solana-vulnerability-scanner live?
The publisher keeps it at https://github.com/trailofbits/skills.
Which agents can use solana-vulnerability-scanner?
Claude, Cursor, and Codex can read a SKILL.md you add from the publisher's repository.