关于 substrate-vulnerability-scanner
面向 Substrate/FRAME 链审计的安全分析技能,帮助开发者检查自定义 pallet 和运行时代码。它围绕 7 类平台特有漏洞提供扫描流程、证据覆盖表及修复建议。
主要功能
- 01检查 7 类 Substrate 漏洞
- 02分析 dispatchable 函数与权限
- 03审查权重和算术安全
- 04输出逐项覆盖表与证据
- 05为发现的问题提供修复建议
适用场景
- 01审计自定义 Substrate pallet
- 02发布前检查链运行时安全
- 03复核未签名交易验证逻辑
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trailofbits/ skills --skill substrate-vulnerability-scanner安装后在 Claude Code 中输入/substrate-vulnerability-scanner确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/trail-of-bits-mark.svgresources/VULNERABILITY_PATTERNS.md详细介绍
Substrate 漏洞扫描器
1. 用途
系统地扫描 Substrate 运行时模块(pallet),以发现可能导致节点崩溃、DoS 攻击或未授权访问的平台特定安全漏洞。此技能涵盖 Substrate/FRAME 链独有的 7 种关键漏洞模式。
2. 何时使用此技能
- 审计自定义 Substrate pallet
- 审查 FRAME 运行时代码
- 对 Substrate 链(Polkadot 平行链、独立链)进行上线前安全评估
- 验证可调度的外部函数
- 审查权重计算函数
- 评估无签名交易验证逻辑
3. 平台检测
文件扩展名和标识
- Rust 文件:
.rs
语言/框架标记
// Substrate/FRAME indicators
#[pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config: frame_system::Config { }
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(10_000)]
pub fn example_function(origin: OriginFor<T>) -> DispatchResult { }
}
}
// Common patterns
DispatchResult, DispatchError
ensure!, ensure_signed, ensure_root
StorageValue, StorageMap, StorageDoubleMap
#[pallet::storage]
#[pallet::call]
#[pallet::weight]
#[pallet::validate_unsigned]
项目结构
pallets/*/lib.rs- pallet 实现runtime/lib.rs- 运行时配置benchmarking.rs- 权重基准测试- 包含
frame-*依赖项的Cargo.toml
工具支持
- cargo-fuzz:Rust 模糊测试
- test-fuzz:基于属性的测试框架
- 基准测试框架:内置权重计算
- try-runtime:运行时迁移测试
4. 此技能的工作方式
调用后,我将:
- 搜索你的代码库,查找 Substrate pallet
- 分析每个 pallet,检查 7 种漏洞模式
- 报告发现的问题,附上文件引用和严重性,并在其上方提供一张包含每种模式判定结果的覆盖率表
- 提供修复方案,解决每个已识别的问题
- 检查权重计算和来源验证
5. 漏洞模式(7 种关键模式)
我会检查 Substrate/FRAME 独有的 7 种关键漏洞模式。有关详细的检测模式、代码示例、缓解措施和测试策略,请参阅 VULNERABILITY_PATTERNS.md。
模式摘要:
-
算术溢出 ⚠️ 严重
- 直接使用
+、-、*、/运算符时,在发布模式下会发生回绕 - 必须使用
checked_*或saturating_*方法 - 影响余额/代币计算、奖励/费用计算
- 直接使用
-
避免 panic ⚠️ 严重 - DoS
- panic 会导致节点停止处理区块
- 不得使用
unwrap()、expect()或未经边界检查的数组索引 - 必须使用
ensure!验证所有用户输入
-
权重和费用 ⚠️ 严重 - DoS
- 权重不正确会导致垃圾信息攻击
- 对成本可变的操作使用固定权重会引发 DoS
- 必须使用基准测试框架,并对所有输入参数设置边界
-
先验证,后写入 ⚠️ 高(v0.9.25 之前)
- 在验证之前写入存储,发生错误时仍会保留(v0.9.25 之前)
- 模式:验证 → 写入 → 发出事件
- 升级到 v0.9.25+,或使用手动
#[transactional]
-
无签名交易验证 ⚠️ 高
- 验证不足会导致垃圾信息/重放攻击
- 优先使用有签名交易
- 如果使用无签名交易:验证参数、防止重放、验证来源
-
不安全的随机性 ⚠️ 中
pallet_randomness_collective_flip容易受到串通攻击- 必须使用 BABE 随机性(
pallet_babe::RandomnessFromOneEpochAgo) - 使用
random(subject),而不是random_seed()
-
来源权限不当 ⚠️ 严重
ensure_signed允许任意用户执行特权操作- 必须使用
ensure_root或自定义来源(ForceOrigin、AdminOrigin) - 必须在运行时中正确配置来源类型
如需查看包含代码示例的完整漏洞模式,请参阅 VULNERABILITY_PATTERNS.md。
6. 扫描工作流
第 1 步:平台识别
- 确认使用了 Substrate/FRAME 框架
- 检查 Substrate 版本(v0.9.25+ 支持事务性存储)
- 定位 pallet 实现(
pallets/*/lib.rs) - 识别运行时配置(
runtime/lib.rs)
步骤 2:可调度分析
对于每个 #[pallet::call] 函数:
- 算术:是否使用了检查或饱和运算?
- Panic:是否没有使用 unwrap/expect/索引操作?
- 权重:是否与成本成正比,输入是否有界?
- Origin:验证级别是否恰当?
- 验证:是否在写入存储前完成所有检查?
步骤 3:Panic 全面检查
# Search for panic-prone patterns
rg "unwrap\(\)" pallets/
rg "expect\(" pallets/
rg "\[.*\]" pallets/ # Array indexing
rg " as u\d+" pallets/ # Type casts
rg "\.unwrap_or" pallets/
步骤 4:算术安全检查
# Find direct arithmetic
rg " \+ |\+=| - |-=| \* |\*=| / |/=" pallets/
# Should find checked/saturating alternatives instead
rg "checked_add|checked_sub|checked_mul|checked_div" pallets/
rg "saturating_add|saturating_sub|saturating_mul" pallets/
步骤 5:权重分析
- 运行基准测试:
cargo test --features runtime-benchmarks - 验证权重是否与计算成本相符
- 检查输入参数是否有界
- 检查权重计算函数
步骤 6:Origin 与权限审查
# Find privileged operations
rg "ensure_signed" pallets/ | grep -E "pause|emergency|admin|force|sudo"
# Should use ensure_root or custom origins
rg "ensure_root|ForceOrigin|AdminOrigin" pallets/
步骤 7:测试审查
- 单元测试覆盖所有可调度项
- 针对 panic 条件的模糊测试
- 用于权重计算的基准测试
- 用于迁移的 try-runtime 测试
步骤 8:报告覆盖情况
报告 §5 中的每种模式,无论是否发现问题。此技能没有发现项模板,因此报告以表格开头,随后列出发现项,并保留全部 7 行:
| # | 模式 | 结论 | 证据 |
|---|---|---|---|
| 1 | 算术溢出 | found | src/lib.rs:212 -- do_transfer 中对 BalanceOf<T> 使用 + |
| 2 | 不要 Panic | ||
| 3 | 权重与费用 | ||
| 4 | 先验证,后写入 | ||
| 5 | 无符号交易验证 | ||
| 6 | 随机数质量差 | ||
| 7 | Origin 不当 |
结论只能是以下之一:
found— 引用file:line,并在表格后完整描述发现项。clear— 此模式适用于该 pallet,且 pallet 已妥善处理。指出你检查过的宏、origin 检查或算术方法,以便读者能够重复检查。n/a— 此模式不适用于此处。用一个短语说明原因(“此 pallet 不接受无符号交易”)。没有检查不等于n/a。注意,模式 4 与版本有关(v0.9.25 之前):说明 pallet 针对的 runtime 版本,而不是省略该行。
少于 7 行的表格属于扫描不完整,必须如实报告。Verdict 单元格为空的行同样属于不完整:上方第 1 行已填写,用来展示格式;完成报告前,每一行都必须按同样方式填写。7 个 clear 结论也是读者可以据此采取行动的结果。只覆盖 3 种模式、对其余 4 种不作说明的报告,看起来与一个没有问题的 pallet 毫无区别,而这正是该表格旨在避免的失败。
7. 优先级指南
严重(需立即修复)
- 算术溢出(代币创建、余额操纵)
- Panic DoS(节点崩溃风险)
- Origin 不当(未经授权的特权操作)
高(上线前修复)
- 权重不正确(通过垃圾请求实施 DoS)
- 违反先验证原则(状态损坏,v0.9.25 之前)
- 无符号验证问题(垃圾请求、重放攻击)
中(在审计中处理)
- 随机数质量差(可能遭到操纵,但影响有限)
8. 测试建议
模糊测试
// Use test-fuzz for property-based testing
#[cfg(test)]
mod tests {
use test_fuzz::test_fuzz;
#[test_fuzz]
fn fuzz_transfer(from: AccountId, to: AccountId, amount: u128) {
// Should never panic
let _ = Pallet::transfer(from, to, amount);
}
#[test_fuzz]
fn fuzz_no_panics(call: Call) {
// No dispatchable should panic
let _ = call.dispatch(origin);
}
}
基准测试
# Run benchmarks to generate weights
cargo build --release --features runtime-benchmarks
./target/release/node benchmark pallet \
--chain dev \
--pallet pallet_example \
--extrinsic "*" \
--steps 50 \
--repeat 20
try-runtime
# Test runtime upgrades
cargo build --release --features try-runtime
try-runtime --runtime ./target/release/wbuild/runtime.wasm \
on-runtime-upgrade live --uri wss://rpc.polkadot.io
9. 补充资源
- 构建安全合约:
building-secure-contracts/not-so-smart-contracts/substrate/ - Substrate 文档:https://docs.substrate.io/
- FRAME 文档:https://paritytech.github.io/substrate/master/frame_support/
- test-fuzz:https://github.com/trailofbits/test-fuzz
- Substrate StackExchange:https://substrate.stackexchange.com/
10. 快速参考清单
完成 Substrate pallet 审计前:
算术安全(严重):
- 可调度函数中不使用直接的
+、-、*、/运算符 - 所有算术运算都使用
checked_*或saturating_* - 类型转换使用
try_into()并处理错误
防止 panic(严重):
- 可调度函数中不使用
unwrap()或expect() - 不进行未经边界检查的数组/切片直接索引
- 使用
ensure!验证所有用户输入 - 除法运算会检查除数是否为零
权重与 DoS(严重):
- 权重与计算成本成正比
- 输入参数设有最大值限制
- 使用基准测试确定权重
- 不存在免费的(零权重)高成本操作
访问控制(严重):
- 特权操作使用
ensure_root或自定义来源 -
ensure_signed仅用于用户级操作 - 在运行时正确配置来源类型
- 在投入生产前移除 Sudo pallet
存储安全(高):
- 使用 Substrate v0.9.25+,或手动添加
#[transactional] - 在写入存储前完成验证
- 操作成功后再发出事件
其他(中):
- 如果可行,无签名交易应使用有签名的替代方案
- 如果使用无签名交易:进行适当的验证、防重放保护和身份验证
- 使用 BABE 随机性(而非 RandomnessCollectiveFlip)
- 随机性使用
random(subject),而非random_seed()
测试:
- 为所有可调度函数编写单元测试
- 使用模糊测试查找 panic
- 生成并验证基准测试
- 为迁移编写 try-runtime 测试
- 输出包含全部 7 行的覆盖率表,每行均标注
found、clear或n/a,并说明原因
11. 应拒绝的合理化说法
- “这个 pallet 很小,所以大多数模式显然都不适用。” 对谁来说显然?写一个
n/a只需一个 从句,就能让判断可供审查。沉默什么也没记录,读者无法分辨这是不适用,还是根本没有检查。 - “编译时没有警告,所以算术运算没问题。” 发布构建会静默溢出。对
Balance使用+就属于模式 1, 无论编译器是否提示;而且生产节点不会运行调试断言。 - “我检查了这个 pallet 中重要的模式。” 判断哪些模式重要,本身就是扫描工作,而不是开始扫描的前提。 应在完成表格后再按严重程度排序,而不是省略某些行。
- “没有发现问题,所以没什么可报告的。” 即使扫描未发现任何问题,也仍要输出完整的覆盖率表。 这张表就是交付内容:它能区分经过审查的 pallet 和只是被粗略看过的 pallet。
- “
ensure_signed是一种来源检查。” 它只能确认有人签了名,而不能确认签名者是否正确。模式 7 关注的是 所需的来源类型;在本应使用ensure_root的地方使用ensure_signed,看似符合这种说法,实际却没有通过检查。 - “权重经过基准测试,所以是正确的。” 基准测试只能测量其实际执行的路径。如果可调度函数的最坏情况取决于存储大小, 或取决于无界循环,即使附有基准测试,也仍属于模式 3。