mcpField
S
solana-vulnerability-scanner

solana-vulnerability-scanner

github.com/trailofbits安全7.9k5.7k 次安装今天更新
代码仓库

关于 solana-vulnerability-scanner

面向原生 Rust 和 Anchor Solana 程序的安全审查技能。检查六类账户模型漏洞,并报告证据、严重程度和修复建议。

主要功能

  1. 01扫描六类 Solana 漏洞模式
  2. 02审查 CPI 程序 ID 验证
  3. 03检查 PDA 和账户验证
  4. 04检查签名者与指令校验
  5. 05附证据和修复建议报告问题

适用场景

  1. 01审计原生 Rust 或 Anchor 程序
  2. 02审查 CPI 和 PDA 实现
  3. 03上线前评估账户校验
  4. 04检查指令内省逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/trailofbits/skills --skill solana-vulnerability-scanner

安装后在 Claude Code 中输入/solana-vulnerability-scanner确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
assets/
trail-of-bits-mark.svg
resources/
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. 此技能的工作方式

调用后,我将:

  1. 搜索你的代码库,查找 Solana/Anchor 程序
  2. 分析每个程序,检查 6 种漏洞模式
  3. 报告发现,附上文件引用和严重程度,并在其上方提供一张包含每种模式判定结果的覆盖表
  4. 提供修复方案,解决发现的每个问题
  5. 检查账户验证和 CPI 安全性

5. 示例输出


6. 漏洞模式(6 种模式)

我会检查 Solana 独有的 6 种关键漏洞模式。详细的检测模式、代码示例、缓解措施和测试策略,请参阅 VULNERABILITY_PATTERNS.md。

模式摘要:
  1. 任意 CPI ⚠️ 严重 - CPI 调用中使用由用户控制的程序 ID
  2. PDA 验证不当 ⚠️ 严重 - 使用 create_program_address 时未使用规范 bump
  3. 缺少所有权检查 ⚠️ 高 - 未验证所有者便对账户进行反序列化
  4. 缺少签名者检查 ⚠️ 严重 - 执行权限操作时未检查 is_signer
  5. Sysvar 账户检查 ⚠️ 高 - Sysvar 账户被伪造(Solana 1.8.1 之前)
  6. 指令内省不当 ⚠️ 中 - 使用绝对索引,导致索引可被重复利用

完整漏洞模式及代码示例请参阅 VULNERABILITY_PATTERNS.md。

7. 扫描工作流

步骤 1:平台识别
  1. 确认 Solana 程序(原生程序或 Anchor)
  2. 检查 Solana 版本(Sysvar 安全性需 1.8.1+)
  3. 定位程序源代码(programs/*/src/lib.rs)
  4. 确认框架(原生程序或 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() 或 Anchor seeds 约束
  • 存储并复用 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任意 CPIn/a此程序不会进行跨程序调用
2PDA 验证不当
3缺少所有权检查
4缺少签名者检查
5Sysvar 账户检查
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. 更多资源


12. 快速参考清单

完成 Solana 程序审计前:

CPI 安全(严重):

  • 所有 CPI 调用在执行 invoke() 前都会验证程序 ID
  • 不能使用用户提供的程序账户
  • Anchor:使用 Program<'info, T> 类型

PDA 安全(严重):

  • PDA 使用 find_program_address() 或 Anchor seeds 约束
  • 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