关于 solana-vulnerability-scanner
面向原生 Rust 和 Anchor Solana 程序的安全审查技能。检查六类账户模型漏洞,并报告证据、严重程度和修复建议。
主要功能
- 01扫描六类 Solana 漏洞模式
- 02审查 CPI 程序 ID 验证
- 03检查 PDA 和账户验证
- 04检查签名者与指令校验
- 05附证据和修复建议报告问题
适用场景
- 01审计原生 Rust 或 Anchor 程序
- 02审查 CPI 和 PDA 实现
- 03上线前评估账户校验
- 04检查指令内省逻辑
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ trailofbits/ skills --skill solana-vulnerability-scanner安装后在 Claude Code 中输入/solana-vulnerability-scanner确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/trail-of-bits-mark.svgresources/VULNERABILITY_PATTERNS.md详细介绍
Solana 漏洞扫描器
1. 用途
系统地扫描 Solana 程序(原生程序和 Anchor 框架),查找与跨程序调用、账户验证和程序派生地址相关的平台特定安全漏洞。本技能针对 Solana 账户模型独有的 6 种关键漏洞模式。
2. 何时使用此技能
- 审计 Solana 程序(原生 Rust 或 Anchor)
- 审查跨程序调用(CPI)逻辑
- 验证程序派生地址(PDA)实现
- 对 Solana 协议进行上线前安全评估
- 审查账户验证模式
- 评估指令内省逻辑
3. 平台检测
文件扩展名和标识
- Rust 文件:
.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- 程序实现Anchor.toml- Anchor 配置- 包含
solana-program或anchor-lang的Cargo.toml tests/- 程序测试
工具支持
- Trail of Bits Solana Lints:用于 Solana 的 Rust 代码检查工具
- 安装:添加到 Cargo.toml
- anchor test:内置测试框架
- Solana Test Validator:本地测试环境
4. 此技能的工作方式
调用后,我将:
- 搜索你的代码库,查找 Solana/Anchor 程序
- 分析每个程序,检查 6 种漏洞模式
- 报告发现,附上文件引用和严重程度,并在其上方提供一张包含每种模式判定结果的覆盖表
- 提供修复方案,解决发现的每个问题
- 检查账户验证和 CPI 安全性
5. 示例输出
6. 漏洞模式(6 种模式)
我会检查 Solana 独有的 6 种关键漏洞模式。详细的检测模式、代码示例、缓解措施和测试策略,请参阅 VULNERABILITY_PATTERNS.md。
模式摘要:
- 任意 CPI ⚠️ 严重 - CPI 调用中使用由用户控制的程序 ID
- PDA 验证不当 ⚠️ 严重 - 使用 create_program_address 时未使用规范 bump
- 缺少所有权检查 ⚠️ 高 - 未验证所有者便对账户进行反序列化
- 缺少签名者检查 ⚠️ 严重 - 执行权限操作时未检查 is_signer
- Sysvar 账户检查 ⚠️ 高 - Sysvar 账户被伪造(Solana 1.8.1 之前)
- 指令内省不当 ⚠️ 中 - 使用绝对索引,导致索引可被重复利用
完整漏洞模式及代码示例请参阅 VULNERABILITY_PATTERNS.md。
7. 扫描工作流
步骤 1:平台识别
- 确认 Solana 程序(原生程序或 Anchor)
- 检查 Solana 版本(Sysvar 安全性需 1.8.1+)
- 定位程序源代码(
programs/*/src/lib.rs) - 确认框架(原生程序或 Anchor)
步骤 2:CPI 安全审查
# Find all CPI calls
rg "invoke\(|invoke_signed\(" programs/
# Check for program ID validation before each
# Should see program ID checks immediately before invoke
对于每次 CPI:
- 调用前验证程序 ID
- 不允许传入由用户控制的程序账户
- Anchor:使用
Program<'info, T>类型
步骤 3:PDA 验证检查
# Find PDA usage
rg "find_program_address|create_program_address" programs/
rg "seeds.*bump" programs/
# Anchor: Check for seeds constraints
rg "#\[account.*seeds" programs/
对于每个 PDA:
- 使用
find_program_address()或 Anchorseeds约束 - 存储并复用 bump seed
- 不使用用户提供的 bump
步骤 4:账户验证检查
# Find account deserialization
rg "try_from_slice|try_deserialize" programs/
# Should see owner checks before deserialization
rg "\.owner\s*==|\.owner\s*!=" programs/
对于每个使用的账户:
- 反序列化前验证所有者
- 检查权限账户的签名者状态
- Anchor:使用
Account<'info, T>和Signer<'info>
步骤 5:指令内省审查
# 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/
- 使用经过检查的函数(Solana 1.8.1+)
- 使用相对索引
- 正确验证关联关系
步骤 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. 报告格式
覆盖情况表
报告 §6 中的每种模式,无论是否发现问题都要报告。将此表放在发现结果上方,并保留全部 6 行:
| # | 模式 | 判定 | 证据 |
|---|---|---|---|
| 1 | 任意 CPI | n/a | 此程序不会进行跨程序调用 |
| 2 | PDA 验证不当 | ||
| 3 | 缺少所有权检查 | ||
| 4 | 缺少签名者检查 | ||
| 5 | Sysvar 账户检查 | ||
| 6 | 指令内省不当 |
判定只能是以下之一:
found— 引用file:line,并在下方完整写出发现结果。clear— 此模式适用于该程序,且程序已妥善处理。指出你搜索过的约束、账户类型或检查项,以便读者重复搜索。n/a— 此处不适用该模式。用一个分句说明原因(“此程序不会进行 CPI 调用”)。没有检查过不等于n/a。模式 5 受版本限制(Solana 1.8.1 之前):引用程序所针对的solana-program版本,不要删掉该行,因为“目标版本为 1.17,上游已修复”和“没有检查”在其他情况下无法区分。
少于 6 行的表格属于未完成的扫描,必须据此报告。判定单元格为空的行同样属于未完成:上方第 1 行已填好,用于展示格式;报告完成前,每一行都必须按相同方式填写。六项判定均为 clear,是读者可以据此采取行动的结果。只涵盖两种模式、对其余四种只字不提的报告,看起来就和程序没有问题时完全一样,而这正是此表旨在避免的问题。
发现结果模板
## [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. 优先级指南
严重(需要立即修复)
- 任意 CPI(攻击者控制的程序执行)
- PDA 验证不当(账户欺骗)
- 缺少签名者检查(未授权访问)
高(上线前修复)
- 缺少所有权检查(伪造账户数据)
- Sysvar 账户检查(身份验证绕过,1.8.1 之前)
中(在审计中处理)
- 指令内省不当(逻辑绕过)
10. 测试建议
单元测试
#[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
}
}
集成测试(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 测试验证器
# Run local validator for testing
solana-test-validator
# Deploy and test program
anchor test
11. 更多资源
- 构建安全合约:
building-secure-contracts/not-so-smart-contracts/solana/ - Trail of Bits Solana Lints:https://github.com/trailofbits/solana-lints
- Anchor 文档:https://www.anchor-lang.com/
- Solana 程序库:https://github.com/solana-labs/solana-program-library
- Solana Cookbook:https://solanacookbook.com/
12. 快速参考清单
完成 Solana 程序审计前:
CPI 安全(严重):
- 所有 CPI 调用在执行
invoke()前都会验证程序 ID - 不能使用用户提供的程序账户
- Anchor:使用
Program<'info, T>类型
PDA 安全(严重):
- PDA 使用
find_program_address()或 Anchorseeds约束 - bump seed 已存储并重复使用(不是由用户提供)
- PDA 账户已针对规范地址进行验证
账户验证(高):
- 所有账户在反序列化前都会检查所有者
- 原生:验证
account.owner == expected_program_id - Anchor:使用
Account<'info, T>类型
签名者验证(严重):
- 所有权限账户都会检查
is_signer - 原生:验证
account.is_signer == true - Anchor:使用
Signer<'info>类型
Sysvar 安全(高):
- 使用 Solana 1.8.1+
- 使用经过检查的函数:
load_instruction_at_checked() - 已验证 Sysvar 地址
指令内省(中):
- 使用相对索引进行关联
- 正确验证相关指令之间的关系
- 同一条指令不能在多次调用中重复使用
测试:
- 单元测试覆盖所有账户验证
- 使用恶意输入进行集成测试
- 已完成本地验证器测试
- 已启用并通过 Trail of Bits lint 检查
- 已输出包含全部 6 行的覆盖率表,每行都标明
found、clear或n/a判定,并说明理由
13. 应拒绝的借口
- “程序很小,所以大多数模式显然不适用。” 对谁而言显然?写一个
n/a只需一句话,还能让判断过程可供审查。保持沉默不会留下任何记录,读者也无法分辨你是检查过,还是根本没检查。 - “Trail of Bits lint 检查通过了,所以程序没有问题。” lint 检查只覆盖这 6 种模式中的一部分。lint 检查全部通过只是其中一行证据,并不能为它从未检查过的模式下结论。请说明它覆盖了哪些模式。
- “我检查了对这个程序重要的模式。” 决定哪些模式重要本身就是扫描工作,而不是开始扫描的前提。完成表格后再按严重程度排序,不要通过漏掉行来筛选。
- “没有发现问题,所以没什么可报告的。” 即使扫描结果为零,也仍须输出完整的覆盖率表。该表就是交付成果:它能区分经过检查的程序和只是粗略看过的程序。
- “Anchor 会处理账户验证。” 请指出具体约束。Anchor 只会验证账户结构中声明的内容,不会多做任何验证:
#[account(mut)]不是所有权检查,而UncheckedAccount则完全跳过检查。请引用属性,而不是框架。 - “PDA 派生方式保证了安全。” 派生不等于验证。不使用规范 bump 的
create_program_address会允许多个有效地址,这正是完整的模式 2。
评论
支持 Markdown