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安全, 可视化界面, 漏洞审计, 网络流量审计, 通知系统, 静态代码扫描