sentio-security/sentio-rs
GitHub: sentio-security/sentio-rs
sentio 是一款基于 AST 的静态安全扫描器,专门用于检测 Solana Anchor 程序中的常见漏洞模式。
Stars: 2 | Forks: 2
# sentio
**基于 AST 的 Anchor 程序安全扫描器。**
sentio 使用 [`syn`](https://docs.rs/syn)(Rust 的宏安全 AST 解析器)扫描 Rust 源文件,以查找常见的 Solana 漏洞模式。它能够理解 Anchor 账户约束、指令逻辑以及 CPI 调用图,从而以极低的误报率产生高信噪比的检测结果。
## 快速开始
```
# 安装
cargo install sentio-cli
# 扫描你的程序
sentio scan /path/to/your/anchor-program
# 使用 JSON 输出进行扫描(用于 CI pipelines)
sentio scan . --format json
# 将 findings 导出为 JSON 文件(通过管道传递给 AI 或工具)
sentio scan . --format json --output report.json
# 仅运行一条特定规则
sentio scan . --rule SW003
# 列出所有可用规则
sentio rules list
```
## CLI 参考
```
sentio
Commands:
scan Scan a Solana program directory or file for vulnerabilities
rules Manage and inspect the built-in rule set
sentio scan [OPTIONS] [PATH]
Arguments:
[PATH] Directory or .rs file to scan [default: .]
Options:
--format Output format: human (default) | json
--output Write JSON output to a file (requires --format json)
--rule Run only a specific rule, e.g. --rule SW003
--include-tests Include test files (excluded by default to reduce noise)
-h, --help Print help
sentio rules list Print all rule IDs and titles
```
**退出码**
| 代码 | 含义 |
|------|---------|
| `0` | 无发现 |
| `1` | 一个或多个发现 |
| `2` | 一个或多个文件解析错误 |
## 输出示例
```
$ sentio scan ./programs/my-program
```
```
==============FINDING 1: SW001 Missing signer check==============
Severity: critical
Location: src/instructions/update.rs:14:1
Rule:
Detects AccountInfo or UncheckedAccount fields whose names suggest an
authority role but have no signer constraint and no is_signer guard.
Matched Because:
Account `authority` appears to be an authority but has no signer constraint
and no is_signer guard; an attacker can pass an unsigned account.
Source:
12| pub vault: Account<'info, Vault>,
13|
>14| #[account(mut)]
| ^
15| pub authority: AccountInfo<'info>,
16|
Guidance:
Use Signer<'info> as the field type, add #[account(signer)], or add
require!(account.is_signer, ...) in the instruction handler.
==============FINDING 2: SW003 Arbitrary CPI target==============
Severity: critical
Location: src/instructions/transfer.rs:29:5
Rule:
Detects CPI calls where no key or program ID check precedes the invocation,
allowing an attacker to supply a malicious program as the CPI target.
Matched Because:
CPI call `invoke` in `handler` has no preceding program key validation.
Source:
27| let ix = build_instruction(&ctx);
28|
>29| invoke(&ix, &[ctx.accounts.target_program.to_account_info()])?;
| ^
30| Ok(())
31|
Guidance:
Add require!(program.key() == expected::ID, ...) before the CPI, or use
Program<'info, T> to enforce program ID validation at the account level.
-------- Summary --------
Total findings: 2
Critical: 2
High: 0
Medium: 0
Low: 0
By rule:
1 SW001 Missing signer check
1 SW003 Arbitrary CPI target
```
## 规则
| ID | 标题 | 严重性 | 捕获内容 |
|----|-------|----------|-----------------|
| SW001 | 缺少 signer 检查 | 严重 (Critical) | 命名为 authority 的 `AccountInfo`/`UncheckedAccount`,且没有 `#[account(signer)]` 和 `is_signer` 守卫 |
| SW002 | 缺少 owner 检查 | 严重 (Critical) | 没有 `owner` 或 `address` 约束,且在 handler 中没有 owner 守卫的 `AccountInfo`/`UncheckedAccount` |
| SW003 | 任意 CPI 目标 | 严重 (Critical) | 没有前置 program key 验证的原始 `invoke`/`invoke_signed` 调用 |
| SW005 | 未检查的算术运算 | 高 (High) | 对没有检查数学运算的账户字段使用 `+`, `-`, `*`, `+=`, `-=`, `*=`;在 release 构建中可能溢出 |
| SW006 | 类型伪装 (Type cosplay) | 严重 (Critical) | 没有判别器检查的 `try_from_slice`;恶意账户类型可能被反序列化为另一种类型 |
| SW008 | 缺少 CPI 后的 reload | 高 (High) | 账户在可能对其进行修改的 CPI 之后被写入,且中间没有执行 `reload()` |
| SW009 | 缺少 token mint 检查 | 高 (High) | 没有 `token::mint` 约束且没有 `associated_token` 的可变 `TokenAccount`,允许错误的 mint 存款 |
| SW010 | 缺少 token owner 检查 | 高 (High) | 没有 `token::authority` 或 authority `has_one` 的可变 `TokenAccount`,允许未经授权的提款 |
| SW011 | 将 AccountInfo 用作数据账户 | 中等 (Medium) | 在需要类型化 `Account<'info, T>` 的地方使用了 `AccountInfo`(存在 init/has_one/seeds 约束) |
| SW012 | PDA 上缺少 seeds + bump | 高 (High) | 带有 `seeds` 但没有 `bump` 的 PDA 账户,跳过了 bump 验证 |
| SW013 | PDA seed 未验证的账户 | 高 (High) | PDA seeds 引用了没有 `owner`、`address` 或 `signer` 约束的同级 `AccountInfo`/`UncheckedAccount` |
| SW014 | PDA bump 不规范 | 中等 (Medium) | `bump = ` 使用了调用方提供的 bump,而不是 Anchor 的规范推导 |
| SW016 | 使用 init_if_needed | 中等 (Medium) | `init_if_needed` 账户可能会被静默重新初始化,从而重置状态 |
| SW018 | 缺少 realloc::zero | 中等 (Medium) | 没有 `realloc::zero = true` 的 `realloc`,在重新分配的内存中留下陈旧数据 |
| SW020 | 将 AccountInfo 用作 CPI program | 中等 (Medium) | 将 `AccountInfo` 用作 CPI program 账户,而不是类型化的 `Program<'info, T>` |
### 内联抑制
在同一行抑制一个发现:
```
#[account(mut)] // sentio-ignore SW001
pub authority: AccountInfo<'info>,
```
在下一行抑制一个发现:
```
// sentio-ignore-next-line SW001
#[account(mut)]
pub authority: AccountInfo<'info>,
```
这两种形式都接受以逗号分隔的规则 ID 列表:`// sentio-ignore SW001, SW002`。
## 工作原理
sentio 的精确性来自于建立在 `syn`(Rust 的宏安全 AST 解析器)之上的两层分析流水线。每条规则都基于代码的实际结构进行操作——即类型化的 AST 节点,而不是源文本。
### 第一层 — Anchor 账户索引
对于每个 `#[derive(Accounts)]` 结构体,sentio 会提取每个字段的类型化模型:
```
AccountInfo named "authority"
type_info → kind: AccountInfo, wrappers: []
constraints → is_signer: false, owner: false, address: false,
init: false, seeds: false, bump: false, ...
```
这由 `anchor_accounts.rs` 构建,它使用 `syn` 的元解析器将 `#[account(...)]` 中的每个键读取为一个强类型的 `AnchorFieldConstraints` 结构体。每个约束——`mut`、`signer`、`has_one`、`seeds`、`bump`、`owner`、`address`、`init`、`init_if_needed`、`realloc`、`realloc::zero`、`close`——都从 AST token 流中被解析为结构体上的类型化字段。
### 第二层 — 指令分析索引
对于文件中的每个函数,sentio 会构建一个包含三项内容的有序模型:
**守卫 (Guards)** — `if` 条件,`require!`,`assert!` 宏。每个守卫都会记录它引用了哪些语义属性:
```
require!(ctx.accounts.authority.is_signer, ErrorCode::Unauthorized);
// → GuardEvidence { references_signer: true, references_key: false, order: 1 }
```
**调用 (Calls)** — 函数和方法调用,被分类为 `Cpi`、`Reload`、`Deserialization` 或 `Other`。CPI 调用还附带一个 `cpi_account_names` 列表——即从 `CpiContext` 结构体解析出的实际账户名称:
```
let cpi_accounts = Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.dest.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
token::transfer(CpiContext::new(token_prog, cpi_accounts), amount)?;
// → CallEvidence { kind: Cpi, cpi_account_names: ["vault", "dest", "authority"], order: 3 }
```
**写入 (Writes)** — 赋值表达式(`=`,`+=`,`-=` 等),其目标以字符串形式捕获:
```
ctx.accounts.game.status = GameStatus::Resolved;
// → WriteEvidence { target: "ctx.accounts.game.status", order: 4 }
```
这三项都标记有连续的 `order` 计数器,以便规则可以推断出什么在什么之前以及什么之后发生。
### 交叉引用分析 (SW008)
CPI 后的 reload 规则是最复杂的。如果没有交叉引用跟踪,CPI 之后的任何写入都会产生一个发现——包括在 token 转账后写入 `game.status = Resolved`,这是一个误报,因为 `game` 根本不是转账的一部分。
sentio 跟踪跨语句的变量绑定来解决这个问题:
1. `let cpi_accounts = Transfer { from: ctx.accounts.vault, ... }` → sentio 在绑定映射中记录 `cpi_accounts → ["vault", "dest", "authority"]`。
2. `let cpi_ctx = CpiContext::new(prog, cpi_accounts)` → sentio 通过绑定映射解析 `cpi_accounts`,将名称转发给 `cpi_ctx`。
3. `token::transfer(cpi_ctx, amount)` → sentio 解析 `cpi_ctx`,赋予该调用 `cpi_account_names: ["vault", "dest", "authority"]`。
4. CPI 之后:`game.status = Resolved` → sentio 提取账户名称 `game`,将其与 `["vault", "dest", "authority"]` 进行核对 → 未找到 → 无发现。
5. CPI 之后:`vault.amount -= fee` → sentio 提取 `vault` → 找到 → 产生发现。
内联模式(`token::transfer(CpiContext::new(prog, Transfer { from: ..., to: ..., authority: ... }), amount)`)也得到了处理——sentio 会遍历嵌套的调用表达式以直接提取结构体字段。
### 规则执行
每条规则接收该文件的 `AnchorAccountsIndex` 和 `InstructionIndex`,并通过布尔逻辑将它们组合起来:
```
SW001: field.type ∈ {AccountInfo, UncheckedAccount}
&& field.name contains "authority" | "admin" | "signer" | "initializer"
&& !constraints.is_signer
&& !constraints.address
&& no guard references_signer && mentions field_name
→ flag
```
没有启发式评分。没有机器学习 (ML)。只有结构化数据和类型化谓词。
### 抑制阶段
在收集完所有规则匹配后,sentio 会运行一个抑制阶段。对于每个发现,它会查找源代码行并检查其是否包含 `// sentio-ignore SWXXX`。被抑制的匹配项会在返回或打印结果之前被丢弃。
## 工作区布局
```
sentio-rs/
├── crates/
│ ├── sentio-core/
│ │ ├── src/
│ │ │ ├── anchor_accounts.rs # Anchor #[account(...)] constraint parser
│ │ │ ├── instruction_analysis.rs # Guard / call / write extractor with CPI cross-reference
│ │ │ ├── rules/
│ │ │ │ └── anchor/ # One module per rule (SW001–SW020)
│ │ │ ├── scanner.rs # File walker + suppression pass
│ │ │ └── syntax.rs # syn parsing wrapper
│ │ └── tests/
│ │ ├── common/mod.rs # Shared fixture helpers
│ │ ├── fixtures/swXXX/ # risky.rs / safe.rs / suppressed.rs per rule
│ │ └── rules_swXXX.rs # Integration test per rule
│ └── sentio-cli/
│ ├── src/
│ │ ├── lib.rs # Public formatter API (render_human_report, etc.)
│ │ └── main.rs # CLI entry point (clap)
│ └── tests/
│ └── human_output.rs # Formatter integration tests
```
## 设计理念
**结构化分析。** sentio 使用 `syn` 解析 Rust 源码——这与过程宏使用的解析器相同——因此每个约束、守卫和表达式都是类型化的 AST 节点。规则针对结构化模型而不是源文本提出疑问:“此字段是否包含没有 `bump` 的 `seeds` 约束?”
**Anchor 感知。** sentio 为 Anchor 的 `#[derive(Accounts)]` 结构体及其完整的约束词汇表——`signer`、`owner`、`address`、`has_one`、`seeds`、`bump`、`init_if_needed`、`realloc::zero` 等建立了模型。它还理解 Anchor CPI 模式,包括 `CpiContext::new` 和账户结构体解析。
**精确率优先于召回率。** 误报会浪费审计人员的时间并削弱对工具的信任。每条规则在发布前都经过了真实程序的验证。当无法保证精确率时,规则会被标记为 `manual review`(人工审查),而不是被视为已确认的漏洞。
**无编译器依赖。** sentio 直接处理原始 `.rs` 源文件。不需要 `rustc_private`,不需要展开 proc-macro,也不需要 `cargo build`。将其指向任何 Solana 程序目录即可运行。
## 状态
sentio 正在积极开发中。规则集正在不断扩充;AST 基础设施已趋于稳定。
**目前已发布 15 条规则**,涵盖了最常见的 Solana/Anchor 漏洞类别。原生 Solana(非 Anchor)规则支持已列入路线图。
标签:Anchor, Rust, Solana, TCP/UDP协议, Web3安全, 可视化界面, 漏洞审计, 网络流量审计, 通知系统, 静态代码扫描