Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
o1js-scan — 用于检测 o1js/Mina zkApps 和 Noir 电路中 zk 电路健全性漏洞的无依赖静态分析器 | Kitploit
工具/GitHubGitHub/auditinfra-io/o1js-scan
防御工具静态分析漏洞扫描器静态代码分析 (SAST)漏洞分析代码分析密码学DevSecOps
GitHubauditinfra-io/o1js-scan

o1js-scan

用于检测 o1js/Mina zkApps 和 Noir 电路中 zk 电路健全性漏洞的无依赖静态分析器

查看仓库
2104天前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
网站

o1js-scan

CI Python License PyPI npm

社区包: o1js-scan 已收录于官方 o1js 社区包 目录中。

最新版本:0.20.0 — 分析器现在可以读取 extends TokenContract 的合约。 在此版本之前,合约匹配仅针对 SmartContract,因此生态系统中所有 同质化代币、NFT 集合和 AMM 池在扫描时都显示为“无发现”。如果你在 0.20.0 之前扫描过代币合约,请重新扫描。 参见 CHANGELOG。

一个快速、无依赖的静态分析器,用于检测以下环境中的 zk 电路可靠性缺陷:

  • o1js / Mina zkApps(TypeScript .ts / .js)— 来自 @method 函数体的 Kimchi 电路
  • Noir(.nr)— Aztec 的类 Rust ZK DSL(包括 aztec-nr 形态的模式)

安全关键缺陷通常不在证明系统中——而在 应用自身的约束中:证明者控制但电路从未绑定的见证。 o1js-scan 是面向 Mina 和 Noir 生态系统中 Circom 同类工具的欠约束信号扫描器。```bash pip install o1js-scan

or: pipx install o1js-scan

or: npm install -D o1js-scan

o1js-scan path/to/zkapp # o1js + Noir (auto) noir-scan path/to/circuits # same binary — Noir-friendly alias noir-scan . --lang noir --fail-on high --sarif noir.sarif

root@kitploit:~
### 示例

给定一个 vault,其 `withdraw` 金额是由 prover 控制的 witness,且从未绑定到链上状态:```console
$ o1js-scan examples/vulnerable_vault.ts --include-examples
LOW      O1JS_UNCONSTRAINED_RECIPIENT       vulnerable_vault.ts:23  fn=withdraw  Recipient `to` is prover-chosen in `withdraw`
HIGH     O1JS_UNCONSTRAINED_WITNESS         vulnerable_vault.ts:23  fn=withdraw  Unconstrained witness `amount` flows to send_amount in `withdraw`
o1js-scan: 2 finding(s) [1 high, 1 low] in 1 of 1 file(s) — fails (--fail-on high)
$ echo $?
1

这里需要 --include-examples 仅仅是因为演示文件位于 examples/ 下,路径分类器默认会降低该目录的级别,这样仓库 自身的示例代码就不会导致构建失败。同样的合约放在你的 src/ 中 会报告 HIGH,无需任何标志。

HIGH 发现是可耗尽漏洞。修复后的合约 (examples/safe_vault.ts)消除了该问题并以 0 退出,仅保留 关于证明者选择接收者的信息性 LOW:```console $ o1js-scan examples/safe_vault.ts --include-examples LOW O1JS_UNCONSTRAINED_RECIPIENT safe_vault.ts:23 fn=withdraw Recipient to is prover-chosen in withdraw o1js-scan: 1 finding(s) [1 low] in 1 of 1 file(s) — passes (--fail-on high) $ echo $? 0

root@kitploit:~
有关 o1js 和 Noir 的易受攻击/已修复配对示例,请参见 [`examples/`](https://github.com/auditinfra-io/o1js-scan/blob/main/examples)。

## 目录

- [安装](#install)
- [用法](#usage) · [抑制已审查的发现](#suppressing-a-reviewed-finding)
- [GitHub Action](#github-action)
- [检测内容 — o1js](#what-it-detects-o1js) · [Noir](#what-it-detects-noir)
- [已知限制](#known-limitations) · [本工具的适用范围止于何处](#where-this-tool-stops)
- [隐私与私有代码](#privacy-and-private-code)
- [后量子审查](#post-quantum-review)
- [兼容性](#compatibility) · [工作原理](#how-it-works)
- [贡献](#roadmap--contributing)

## 安装```bash
pip install o1js-scan

对于隔离的全局 CLI 安装,请使用 pipx:```bash pipx install o1js-scan

root@kitploit:~
对于基于 Node/npm 的 Noir、Aztec 或 o1js 应用仓库,请安装 npm 封装包:```bash
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high

npm 包是对同一个 Python 分析器的轻量封装,需要在 PATH 中提供 Python 3.8+(python3 或 python)。设置 O1JS_SCAN_PYTHON 以选择特定的解释器。

或者从源码安装:```bash git clone https://github.com/auditinfra-io/o1js-scan cd o1js-scan pip install -e .

root@kitploit:~
无第三方 Python 依赖。Python 3.8+。`noir-scan` 控制台脚本与 `o1js-scan` 一同安装(同一入口点),包括通过 npm 包装器安装。

## 用法```bash
# scan a directory (recursively; skips node_modules, target/, .git, …)
o1js-scan path/to/project

# Noir-only / o1js-only
noir-scan circuits --lang noir
o1js-scan src --lang o1js

# scan a single file
o1js-scan src/MyContract.ts
noir-scan src/main.nr

# machine-readable output for CI
o1js-scan src --json

# SARIF 2.1.0 for GitHub code scanning (writes o1js-scan.sarif by default)
o1js-scan src --sarif
noir-scan . --lang noir --sarif noir.sarif

# choose which severity fails CI (critical|high|medium|low|none; default high)
o1js-scan src --fail-on medium

# progressive/power-user gate (equivalent to --fail-on medium)
o1js-scan src --strict

# test code is excluded by default (both backends); opt back in
o1js-scan src --include-tests

# example code is downgraded to LOW by default; keep original severity
o1js-scan src --include-examples

o1js-scan --version

当存在达到或高于 --fail-on 级别(默认 high)的发现时,退出码为 1,否则为 0 —— 因此你可以直接将其接入 CI。在默认设置下,低/中等级别的发现(包括下面的信息性接收者规则)不会导致构建失败;使用 --fail-on none 仅报告,或使用 --strict(--fail-on medium 的简写)进行更严格的把关,同时仍将低严重性发现视为建议性。这两个选项互斥,因此 CI 配置不会产生歧义。缺失扫描路径会以退出码 2 退出,并在 stderr 上输出错误,因此拼写错误不会静默地让 CI 通过为一次干净的运行。每次运行都会向 stderr 打印一行摘要(按严重性计数以及门控判定)。

测试代码默认被排除 —— 两个后端均如此。 测试会故意构造无效值和错误交易以证明断言会拒绝它们,因此那里的发现正是测试的目的,而不是电路缺陷。当满足以下条件时,文件被视为测试代码:

  • 其名称匹配 *.test.ts / *.spec.ts(以及 .js/.jsx/.tsx/.mjs/.cjs 变体),或 *_test.nr / test_*.nr;
  • 它位于 test/、tests/、__tests__/、spec/ 或 __mocks__/ 目录下;
  • (仅 Noir,基于内容)函数带有 #[test] / #[test(...)] 属性,或位于 / 块内 —— 块作用域,因此生产文件末尾的测试模块不会使文件其余部分静默。

传入 --include-tests 以报告它们。

示例代码被降级,而非丢弃。 位于 examples/ 或 example/ 目录中,或位于名为 *.eg.ts(以及 .nr 和其他 JS/TS 扩展名)的文件中的发现,会被降为 LOW 并附注 —— 仍会报告,但不再能使构建失败。示例代码是故意简化的,将框架自身的示例标记为漏洞是噪音;但它被复制到生产环境的频率远高于测试代码,这就是它被降级而非隐藏的原因。传入 --include-examples 以保留原始严重性。

每当任一策略适用时,运行都会向 stderr 打印一行说明 —— 例如 6 file(s) skipped as test code, 1 finding(s) downgraded as examples —— 因此安静的扫描永远不会静默无声。计数也会出现在 SARIF 的 invocation.properties 下。注意权衡:检测仅基于路径(不解析 describe(/it(),因此存储在 tests/ 下的生产电路会被跳过 —— stderr 行就是你注意到它的方式。

遍历树时跳过的目录: node_modules、target(nargo)、.git、dist、build、__pycache__、.venv、venv。

抑制已审查的发现

在不放宽门控的情况下,通过在被标记行上或其上一行添加内联注释,使已分类的发现静默:```ts this.send({ to, amount }); // o1js-scan-disable-line O1JS_UNCONSTRAINED_WITNESS

// o1js-scan-disable-next-line this.send({ to, amount });

root@kitploit:~
- **`--no-verify`**:跳过 SSL 证书验证(默认:`false`)
- **`--timeout`**:请求超时时间(秒)(默认:`10`)
- **`--user-agent`**:自定义 User-Agent 字符串
- **`--proxy`**:用于请求的代理 URL
- **`--headers`**:自定义请求头(JSON 格式)
- **`--cookies`**:自定义 Cookie(JSON 格式)
- **`--follow-redirects`**:跟随 HTTP 重定向(默认:`true`)
- **`--max-redirects`**:最大重定向次数(默认:`5`)
- **`--retry`**:失败请求的重试次数(默认:`3`)
- **`--retry-delay`**:重试之间的延迟时间(秒)(默认:`1`)
- **`--concurrency`**:最大并发请求数(默认:`10`)
- **`--rate-limit`**:每秒最大请求数(默认:`0` = 无限制)
- **`--random-agent`**:为每个请求使用随机 User-Agent
- **`--delay`**:请求之间的延迟时间(秒)(默认:`0`)
- **`--jitter`**:添加到延迟的随机抖动(秒)(默认:`0`)
- **`--encoding`**:响应编码(默认:`utf-8`)
- **`--no-color`**:禁用彩色输出
- **`--quiet`**:抑制除结果外的所有输出
- **`--verbose`**:启用详细输出
- **`--debug`**:启用调试输出
- **`--log-file`**:日志文件路径
- **`--log-level`**:日志级别(默认:`INFO`)
- **`--output`**:输出文件路径
- **`--output-format`**:输出格式(`text`、`json`、`csv`、`html`)(默认:`text`)
- **`--config`**:配置文件路径
- **`--save-config`**:将当前配置保存到文件
- **`--version`**:显示版本信息并退出
- **`--help`**:显示帮助信息并退出```nr
let inv = unsafe { hint(x) };  // o1js-scan-disable-line NOIR_UNCONSTRAINED_WITNESS

列出一个或多个规则 ID 以仅抑制这些规则;一个裸指令(无 ID)会抑制目标行上的所有规则。

作为库使用:```python from o1js_scan import analyze_file, analyze_project

for path, finding in analyze_project("src", lang="auto"): print(path, finding.rule_id, finding.severity.value, finding.title)

root@kitploit:~
## GitHub Action

只需几行即可将扫描器添加到 CI 中。发现的问题会以注释形式显示在 PR diff 上,并作为警报出现在仓库的 **Security → Code scanning** 标签页中。```yaml
# .github/workflows/o1js-scan.yml
name: o1js-scan
on: [push, pull_request]

permissions:
  contents: read
  security-events: write   # required to upload SARIF to code scanning

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: auditinfra-io/[email protected]
        with:
          path: src              # optional, defaults to the repo root
          lang: auto             # auto | o1js | noir
          # version: 0.20.0       # optional, pin the scanner version
          # fail-on: high         # optional, fail the job on high/critical

仅 Noir 的 CI 配方

推荐给希望获得代码扫描告警和高严重性 门禁的 Noir 项目:```yaml

  • uses: auditinfra-io/[email protected] with: path: . lang: noir fail-on: high
root@kitploit:~
或者不使用 Action:```bash
pip install o1js-scan
noir-scan . --lang noir --fail-on high --sarif noir.sarif

pre-commit(可选)```yaml

.pre-commit-config.yaml

  • repo: local hooks:
    • id: noir-scan name: noir-scan entry: noir-scan language: system pass_filenames: false args: [".", "--lang", "noir", "--fail-on", "high"]
root@kitploit:~
输入:`path`(默认 `.`)、`lang`(`auto`|`o1js`|`noir`,默认 `auto`)、
`version`(要安装的 PyPI 版本,默认最新)、`upload-sarif`(默认
`true`)、`fail-on`(`critical`|`high`|`medium`|`low`|`none`,默认 `none`)、
`fail-on-findings`(已弃用,默认 `false`)、`include-tests`(默认
`false`)、`include-examples`(默认 `false`)。输出:`sarif-file`。SARIF
上传需要 `security-events: write` 并启用代码扫描。

报告和门禁由同一个参数数组构建,因此 `include-tests`
和 `include-examples` 同时作用于两者——你读取的 SARIF 和你据以设置门禁的退出码
始终描述同一组源文件。报告阶段以
`--fail-on none` 运行,因此发现项永远不会阻止 SARIF 上传,但操作性
失败(路径不存在、CLI 用法错误)仍会使该步骤失败,
而不是被报告为一次干净的扫描。

`fail-on-findings: true` 为兼容性而保留,当 `fail-on` 保持为 `none` 时映射为 `fail-on: high`;它会发出弃用警告。优先使用
`fail-on`,它可以在任意严重级别设置门禁。

## 它检测什么(o1js)

### 支持的规则一览

<!-- BEGIN GENERATED RULE SUMMARY -->
| 后端 | 规则数 | 高严重级能力 | 中严重级能力 | 低严重级能力 |
|---------|------:|-------------:|---------------:|------------:|
| o1js | 18 | 11 | 12 | 2 |
| Noir | 11 | 4 | 9 | 1 |
| **总计** | **29** | **15** | **21** | **3** |
<!-- END GENERATED RULE SUMMARY -->

计数是每个后端支持的不同规则 ID 数量。根据上下文分配
严重级别的规则(例如,对价值转移为高,对状态写入为中)会出现在多个严重级别列中,因此
严重级别列有意不等于规则总数。目前
没有 critical 或 info 严重级别的规则。完整描述和
误报防护如下。

<!-- BEGIN GENERATED O1JS RULE TABLE -->
| 规则 | 严重级别 | 含义 |
|------|----------|---------------|
| `O1JS_MISSING_STATE_PRECONDITION` | high | `this.x.get()` 读取时没有匹配的 `requireEquals(...)` / `getAndRequireEquals()`。裸 `get()` 不添加**任何**账户前置条件,因此证明不会将 `x` 绑定到其链上值——证明者可以替换任意值。 |
| `O1JS_UNCONSTRAINED_WITNESS` | high / medium | 一个 `@method` 参数(证明者控制的私有见证)流入发送**金额**(`this.send(...)` 或同一方法内的 `AccountUpdate.create*(...).send(...)`)或状态 `.set(...)`,并且**从未**被断言。这是约束不足的 Circom 信号的直接类比。当它到达价值转移时为高。 |
| `O1JS_UNCONSTRAINED_PROVABLE_WITNESS` | high / medium / low | 一个 `Provable.witness(...)` 局部变量流入发送/状态效果,且**没有**电路内断言。见证回调在电路*之外*运行(它只是证明者提示),因此结果是证明者控制的新值——这是除 `@method` 参数之外的另一个见证来源。它必须被重新推导并断言(`x.assertEquals(<recomputed>)`)或绑定到状态。在发送金额(`this.send(...)` 或同一方法内的 `AccountUpdate.create*`)上为高。 |
| `O1JS_UNCONSTRAINED_RECIPIENT` | low | 一个 `@method` 参数**仅**用作发送的 `to:` 接收者。这通常是故意的(用户指定自己的提款目的地),属于信息性提示——只有当目的地本应是固定金库或状态记录的地址时才有意义。**不会**触发 CI 退出码门禁。 |
| `O1JS_WITNESS_NOT_BOUND_TO_STATE` | medium | 一个见证在效果之前仅被*平凡地*约束(例如 `> 0`,或与常量比较)——从未绑定到链上状态。确认链下编排使其安全,否则余额可被抽干至其现有值。 |
| `O1JS_STALE_MERKLE_ROOT` | high | 一个方法从证明者提供的见证(`computeRootAndKey` / `calculateRoot`)重新计算 Merkle 根,但没有将任何重新计算的根绑定到当前链上根。如果没有针对实时根的 `this.root.requireEquals(...)` / `assertEquals`,证明者可以传入伪造或过期树的见证——伪造成员资格或重放旧状态。绑定可能位于未装饰的同类辅助函数(`this.verifyX(witness)`)中;辅助函数传播覆盖了这种情况。 |
| `O1JS_UNVERIFIED_PROOF` | high | 一个类型为 `Proof<...>` / `SelfProof<...>` / `DynamicProof<...>` 的 `@method` 参数在其公共字段被使用之前从未被 `.verify()`。传入 Proof 并不会验证它——如果没有显式验证,证明者可以提供任意证明对象,并且对其 `publicOutput` 的任何使用都是不受约束的。当 `.verifyIf(flag)` 由不受约束的 `@method` 参数门控且证明的公共字段被读取时也会触发,因为证明者可以使条件为假。 |
| `O1JS_UNASSERTED_BOOL` | high / medium | 一个 o1js 谓词(`equals` / `lessThanOrEqual` / …)返回 `Bool`,除非结果被断言或使用,否则**不添加**任何约束。当调用是裸的丢弃语句时为 HIGH;当赋值给一个之后从未被引用的局部变量时为 MEDIUM。 |
| `O1JS_UNCONSTRAINED_SENDER` | high / medium | `this.sender.getUnconstrained()` 返回交易发送者而不证明它。当该值(或由它派生的局部变量)流入断言 / 状态 `.set` / `send`(空洞检查)时为 HIGH;否则为 MEDIUM。优先使用 `this.sender.getAndRequireSignature()`,或展开的惯用法 `AccountUpdate.createSigned(sender)`。**在以下情况保持安静**:(1) 同一 `@method` 也在任何位置调用了 `this.sender.getAndRequireSignature()`(签名要求是方法作用域的),或 (2) 见证的发送者值是 `AccountUpdate.createSigned(...)` 的参数 / 对同一密钥的 `AccountUpdate.create(...).requireSignature()`(要求参数同一性——对不同密钥的 `createSigned` 不会抑制)。 |
| `MissingRangeCheck` | high | 一个原始 `Field`(不是经过范围检查的 `UInt64`/`UInt32`)被用作转移金额。`Field` 是模 p 的元素,不受范围限制。 |
| `O1JS_WEAK_PERMISSIONS` | high / medium | `editState` / `send` 设置为 `proofOrSignature()` 或 `none()`,让 zkApp 账户密钥通过签名绕过电路。还会标记 `setVerificationKey` / `setPermissions` 保持为 `signature` / `proofOrSignature` / `none`(Mina 文档所述的升级辅助轮);当与同一 `permissions.set` 中弱的 `editState`/`send` 组合时为 HIGH。 |
| `O1JS_LOGIC_OUTSIDE_PROOF` | high | 安全逻辑(assert / approve / send / 状态 `.set`)位于 `Provable.asProver(...)` 或 `Provable.witness*` 回调内。这些回调在电路*之外*运行——恶意证明者可以删除它们并仍产生可验证的证明。 |
| `O1JS_APPROVE_WITHOUT_BINDING` | medium | 一个 `@method` 调用 `approve` / `approveAccountUpdate` / `approveBase` 而不读取 `balanceChange` / `publicKey`,也没有 `assertCanMint` / `assertCanBurn` / `forEachUpdate` 守恒检查——即 Mina FlawedTokenContract 原型。 |
| `O1JS_VACUOUS_ASSERT` | high / medium | 一个构造上即满足的断言:`x.assertEquals(x)`、`x.equals(x).assertTrue()` 或 `Bool(true).assertTrue()`。自比较为 HIGH(几乎总是笔误);常量 Bool 断言为 MEDIUM。 |
| `O1JS_CONDITIONAL_ASSERT` | medium | 一个位于 `if <flag> { ... }` 内的断言,其中 `<flag>` 是证明者控制的 `@method` `Bool`(或来自 `.toBoolean()` 的局部变量)。JS 条件语句不会像 `Provable.if` 那样约束电路。内联比较为精确性而保持不报告。 |
| `O1JS_GUARDED_INVERSE` | medium | 一个位于 `Provable.if` 分支内的 `.div()` / `.inv()` / `.sqrt()`,由针对其失败值的条件守护。两个分支都在电路内求值,并且这些调用无条件地断言逆或根存在,因此守护不会跳过断言——电路对于守护本要处理的那个输入恰好不可满足,并且该方法永远无法为其证明。由 Veridise 报告为 `V-O1J-VUL-060`。先计算安全除数(`Provable.if(isZero, Field(1), d)`),然后选择结果。**在以下情况保持安静**:守护对除数没有任何说明,因此围绕安全除法的无关 `Provable.if` 不会被标记。 |
| `O1JS_PRECONDITION_OVERWRITTEN` | medium | 在一个方法中对**同一**属性有两个或更多 `requireEquals` / `requireBetween` / `requireNothing` 调用,且参数不同。前置条件是*设置*在 AccountUpdate 上而非累积,因此每次调用都会覆盖前一次,只有最后一次被强制执行——不像电路内断言那样组合。`a.requireEquals(b)` 然后 `a.requireEquals(c)` 意味着 `a === c`,而不是 `a === b`。由 Veridise 报告为 `V-O1J-VUL-012`。**在以下情况保持安静**:参数相同(幂等,无损失)、在 `getAndRequireEquals()` 上(不同方法,因此重复状态读取没问题),以及调用位于互斥的 JS 分支中(在电路构建时解析)。最后一项豁免可能隐藏跨越无关 `if`/`else` 的真实覆盖。 |
| `O1JS_STATE_READ_AFTER_WRITE` | medium | 一个 `@state` 字段在同一方法中对同一字段的 `set(...)` 完成后被读取(`get()` / `getAndRequireEquals()`)。`set()` 在 AccountUpdate 上记录更改但不会写穿到 `get()`,因此读取仍观察到写入之前的值,任何基于它的算术都会因该写入而静默偏差。由 Veridise 报告为 `V-O1J-VUL-030`。将新值保存在局部变量中,而不是读回状态。**在以下情况保持安静**:读取嵌套在写入自身的参数内(读-改-写惯用法 `this.x.set(this.x.getAndRequireEquals().add(1))`,这是正确的),以及写入和读取位于互斥的 JS 分支中。作用域限于单个方法——Veridise 也描述的跨方法缓存情况需要此规则不具备的调用图知识。 |
<!-- END GENERATED O1JS RULE TABLE -->

### 误报防护(o1js)

分析器设计为对正确代码保持安静:

- **签名门控方法被跳过。** 一个调用
  `this.requireSignature()`(或 `getAndRequireSignature`、`AccountUpdate.createSigned`、
  `Signature.verify`)的 `@method` 是所有者/管理员门控的——其参数由密钥
  持有者选择,而非任意证明者——因此其见证不会被标记。这是
  o1js 中 `onlyOwner` 的等价物。
- **状态绑定的见证被跳过。** 一个被断言等于(或通过排序比较
  受限于)`getAndRequireEquals()` 派生值的参数是可靠的,不会被报告。这涵盖直接形式——
  `amount.assertLessThanOrEqual(bal)`——以及链式形式
  `amount.lessThanOrEqual(bal).assertTrue()`。位于
  未装饰的同类辅助函数(`this.verifyX(arg)`)中的绑定也被识别,
  包括通过此类辅助函数的链。
- **已验证的证明被跳过。** 一个 `Proof` / `SelfProof` / `DynamicProof` /
  `*Proof` 类型的参数,若其上调用了 `.verify()`,则受已验证电路约束——对其(及其 `publicOutput` /
  `publicInput`)的见证发现被抑制。`.verifyIf(flag)` 仅在
  条件不是不受约束的方法参数,或本身被断言时才被认可。这同样适用于规范的 OffchainState 包装器
  `this.offchainState.settle(proof)`(框架在 `settle` 内部验证)。
  手工编写的 `.settle(proof)` **不**被假定为验证。相反情况(证明类型参数从未验证
  且未 OffchainState 结算)被报告为 `O1JS_UNVERIFIED_PROOF`。
- **已断言/已使用的 Bool 被跳过。** 一个用
  `.assertTrue()` / `.assertFalse()` 链式调用、嵌套在 `Provable.if(...)` 中,或
  赋值给之后被引用的局部变量的谓词,不会被报告为
  `O1JS_UNASSERTED_BOOL`。
- **已认证的发送者被跳过。** `this.sender.getUnconstrained()`
  在同一 `@method` 也调用
  `this.sender.getAndRequireSignature()` 时不会触发,或当该见证值被
  传递给 `AccountUpdate.createSigned(...)` / 通过
  由它构建的 AccountUpdate 上的 `.requireSignature()` 认证时(要求参数
  同一性)。
- 注释和字符串字面量在分析前被剥离,因此字符串内的 `assert`
  不会产生错误结果。

## 它检测什么(Noir)

同样的可靠性理念——约束不足的见证——适用于
[Noir](https://noir-lang.org)(`.nr`)电路。将扫描器指向 `.nr`
文件(或使用 `--lang noir`),它会用 Noir 规则集分析它们。
相同的词法、无依赖方法。针对 aztec-nr oracle /
`unsafe` 惯用法进行了校准——参见 [`docs/noir_calibration.md`](https://github.com/auditinfra-io/o1js-scan/blob/main/docs/noir_calibration.md)。

<!-- BEGIN GENERATED NOIR RULE TABLE -->
| 规则 | 严重级别 | 含义 |
|------|----------|---------------|
| `NOIR_UNCONSTRAINED_WITNESS` | high | 一个从 `unsafe { ... }` 块绑定的值——`unconstrained fn`(oracle / Brillig 提示)的结果——从未被 `assert` / `assert_eq`(或确认辅助函数 / merkle 检查)重新约束。提示在电路*之外*运行。`O1JS_UNCONSTRAINED_PROVABLE_WITNESS` 的类比。 |
| `NOIR_UNCONSTRAINED_INPUT` | medium | `fn main` 的一个私有(见证)输入,它不流入**任何** `assert` / `assert_eq`,并且**不是**公共输出的一部分。`O1JS_UNCONSTRAINED_WITNESS` 的类比。 |
| `NOIR_UNCONSTRAINED_PUBLIC_INPUT` | medium | `fn main` 的一个**公共**输入,它不触及任何约束也不触及任何输出——电路从不读取它。这是私有见证规则的*对偶*:验证者提供该值并相信语句是关于它的,而电路忽略它(例如一个从未被检查的 `merkle_root: pub Field`,因此成员资格从未真正被证明)。为 MEDIUM,因为故意未使用的公共输入也是将证明绑定到上下文(nonce / chain id / recipient)的合法惯用法,这在词法上无法区分——因此它不会在默认 `--fail-on high` 下门控 CI。 |
| `NOIR_UNCHECKED_CAST` | medium | 一个证明者控制的值被转换为窄无符号类型(`as u8`/`u16`/`u32`)且**没有**范围断言。o1js `MissingRangeCheck` 的类比。 |
| `NOIR_UNCONSTRAINED_ARRAY_INDEX` | medium | 一个证明者控制的值被用作数组索引(`arr[i]`)且对其**没有任何**检查。Noir 的隐式边界检查仅确立索引*在范围内*——而非它是*正确的*索引——因此证明者仍可自由选择任意元素并仍产生可验证的证明。这是 Merkle 路径位置、note 选择和允许列表成员资格背后的选择器自由 bug。当索引受范围限制、被相等性固定、在转换前受限(`index.assert_max_bit_size::<8>(); let i = index as u32;`),或读回的值本身被 `assert_eq` 固定时被抑制。 |
| `NOIR_UNASSERTED_BOOL` | high / medium | 一个比较,其 `bool` 结果被**丢弃**。o1js `O1JS_UNASSERTED_BOOL` 的类比。 |
| `NOIR_CONDITIONAL_ASSERT` | medium | 一个位于 `if <flag> { ... }` 内的 `assert`,其中 `<flag>` 是证明者控制的裸 `bool` 或从证明者控制值派生的局部变量。条件内的约束仅在条件为真时适用,因此证明者选择的分支可以跳过检查。内联比较(`if x != 0`)为精确性而保持不动;将守护赋值给局部变量(`let gate = x != 0; if gate`)会被报告,除非 `gate` 本身被断言。 |
| `NOIR_CONDITIONAL_CONSTRAIN` | medium | 一个 `constrain_*` / `confirm_*` / `verify_*` 调用仅在证明者控制的 `if` 下,而 `unsafe` 提示仍到达输出。 |
| `NOIR_UNUSED_CHECK_RESULT` | high / medium | 一个 `check_*` / `confirm_*` / `verify_*` / `constrain_*` 结果被丢弃(裸调用)或赋值后从未被断言——检查不绑定电路。 |
| `NOIR_VACUOUS_CONSTRAINT` | high / medium | 一个构造上即满足的约束:自比较(`assert(x == x)`、`assert_eq(x, x)`、`x >= x`)或常量条件(`assert(true)`)。它不添加任何限制,但该行*读起来*像检查——这使其比缺失约束更危险,因为审查到此为止。自比较为 HIGH(几乎总是真实检查的笔误:`assert(computed == expected)` 误写为 `assert(expected == expected)`);常量为 MEDIUM,更常是占位符。`x != x` **不**被标记——那是不可满足的,是活性 bug 而非静默的可靠性漏洞。 |
| `NOIR_UNSAFE_MISSING_SAFETY` | low | 一个 `unsafe { ... }` 块没有相邻的 `// Safety:` 注释。信息性;在默认 `--fail-on high` 下不会使 CI 失败。 |
<!-- END GENERATED NOIR RULE TABLE -->

### 误报防护(Noir)

- **Assert / let 跳转 / 同文件确认辅助函数**绑定 `unsafe` 提示。
- **调用点名称** `constrain_*` / `confirm_*` / `verify_*` /
  `check_(non_)membership*` / `public_data_storage_read` 认可参数(对丢弃的检查进行未使用结果检测)。
- **文档化的有意不受约束**(需要相邻 `// Safety:`):
  `random()`、`avm::…`,以及 kernel/rollup/discovery 延迟措辞。
- **元组 `let` + 已断言标志**绑定传入成员资格检查的 merkle 见证。

示例:```console
$ noir-scan examples/noir_unconstrained.nr --include-examples
HIGH     NOIR_UNCONSTRAINED_WITNESS         noir_unconstrained.nr:16  fn=main  Unconstrained `unsafe` result `inv` in `main`
LOW      NOIR_UNSAFE_MISSING_SAFETY         noir_unconstrained.nr:16  fn=  `unsafe` block without a `// Safety:` comment
noir-scan: 2 finding(s) [1 high, 1 low] in 1 of 1 file(s) — fails (--fail-on high)

$ noir-scan examples/noir_constrained.nr --include-examples
noir-scan: no findings in 1 o1js or Noir file(s) — passes (--fail-on high)

与上面的 o1js 示例一样,--include-examples 仅在需要时才使用,因为这些演示文件位于 examples/ 下。

已知限制

该分析器是一个无依赖的词法前端加上轻量级语义层,通过同类辅助函数进行别名跟踪和过程间传播。它不是 TypeScript 编译器前端、类型检查器或全程序数据流引擎,并且此扫描器中不存在 SMT 或形式化证明层。 在分类时请记住这些盲点——对于这种无依赖设计而言,它们是已知且有意为之的,并非缺陷:

  • 仅跟踪简单别名。 见证跟踪会跟踪同一方法中的普通别名,例如 const q = qty,但不会跟踪派生表达式或解构: ```ts const q = qty; this.send({ to: dest, amount: q }); // followed const q = qty.add(1); this.send({ to: dest, amount: q }); // not followed const slot = this.root; slot.get(); // missing precondition missed

    root@kitploit:~
  • 跨方法绑定仅覆盖同类辅助链。 一个未装饰的同类辅助函数以 this.verifyX(arg) 形式调用时,可以状态绑定调用方的参数,并且自 0.19.0 起,这些辅助函数的链(@method → 辅助函数 A → 辅助函数 B)会被追踪至不动点。辅助函数→辅助函数这一步骤仅映射裸参数引用,因此 helperA(x.add(1)) 不会传播。自由函数和导入函数仍然不会被追踪,而辅助函数参数的局部变量别名仍是一个已记录的局限。

  • 未断言布尔检测是基于语句形态的。 Tier A 仅标记最外层调用为布尔谓词且其后没有任何链式调用的裸表达式语句。嵌套在 Provable.if(...) 内部的谓词,或被赋值后稍后使用的谓词,不会被标记。布尔局部变量的复杂控制流用法如果该名称从未被引用,仍可能被遗漏(失败模式:漏报,而非误报)。

  • 签名门控是方法级且基于子字符串的。 _method_is_signature_gated 将整个 @method 视为所有者门控,只要它包含签名惯用法,并且仅当接收者名称字面包含 signature 时才识别验证器——因此 sig.verify(admin, msg) 不会被识别为门控,而大型方法中其他地方无关的签名检查可能导致过度抑制。它对每个方法是全有或全无的。

这些就是为什么发现结果是人工审查的起点,而非证明。数据流感知的重写被有意排除在词法分析器的范围之外。

本工具的边界

o1js-scan 有意设计为浅层、单文件词法遍历——无解析器、无数据流、无求解器。这正是它无依赖且在 CI 中即时运行的原因,同时也是硬性上限。上述局限并非待办事项,而是设计的必然结果。

因此,明确说明本工具能告诉你什么、不能告诉你什么是值得的:

  • 一次干净的运行不是审计。 它意味着此扫描器识别的任何形态都未匹配——而非电路是健全的。需要数据流、路径敏感性或约束求解的缺陷类别,对于这种形态的工具而言,在任何语言中都是无法触及的。
  • 一个发现结果是线索,而非裁决。 此处的每条规则都是带有已记录误报类别的启发式规则。

对于你在每次提交时运行的 linter 而言,这种权衡是正确的。如果你正在处理差异至关重要的事情——一个持有真实价值的协议、一个你无法承受出错的电路——请将此视为第一遍,并为真正的审查预留预算。

如需更深入的分析,独立的完整扫描器维护在 audit-engine-cli 仓库中。 o1js-scan 是有意轻量化的开源扫描器;完整扫描器的专有检测知识和实现细节不在此处复现。如需访问或更完整的电路审查,请联系: [email protected]。

隐私与私有代码

已安装的 CLI 在本地分析文件。它没有遥测、网络客户端、账户或上传步骤,其 Python 运行时没有第三方依赖。运行 o1js-scan path/to/private-repo 不会将源代码或发现结果发送到任何地方。

与编译器日志一样,扫描器输出可能包含路径、标识符和源代码片段。SARIF 还会标识确切的仓库位置,而 GitHub Action 会将其上传到 GitHub 代码扫描。请使用你已用于被扫描源代码的相同仓库和 CI 访问控制。

想在不分享应用程序的情况下贡献有用的误报或漏报报告?用虚构的名称和常量复现语法,逐条语句移除业务逻辑,并在发布前验证合成片段仍能触发相同的规则。 隐私安全贡献指南提供了具体的检查清单,以及多种在不披露私有电路的情况下帮助 o1js 社区的方式。

这一边界并不妨碍开源扫描器变得更好。公开的 o1js 文档和仓库可以支持新规则和兼容性测试夹具;合成示例可以测试误报和遗漏的约束;解析器韧性、诊断、SARIF、性能、打包和校准都可以在不发布私有审计技术或客户代码的情况下改进。开源扫描器应做出可独立解释的声明;私有研究可以保留在单独的审计引擎中。

后量子审查

量子风险与电路安全相关,但它不是一条缺失约束规则。o1js-scan 不判断签名、哈希、承诺、Kimchi 证明系统或 Mina 本身是否满足后量子安全目标。这些答案取决于具体的原语和参数、平台假设、部署所需生命周期及其迁移计划——而不仅仅是词法扫描器能看到的 TypeScript 标识符。

受 O(1) Labs 的Qubit or Not Qubit启发, 后量子审查指南将这一边界转化为 o1js 特定的清单和加密敏捷性检查表。请将其与本扫描器配合使用,而不要将干净的扫描解释为后量子评估。

兼容性

适用于 o1js 1.x、2.x 和 3.x,包括 o1js 3.0.0 所针对的 Mesa 硬分叉。o1js-scan 将 TypeScript 源代码作为文本分析,并且对 o1js 没有运行时依赖——没有任何版本固定。它基于现代 require* 前置条件 API(getAndRequireEquals、requireEquals、requireSignature、getAndRequireSignature)、@method / @method() / @method.returns(...) 装饰器、带注解的 @state 字段、this.send({...})、底层 AccountUpdate.balance.subInPlace(...) 转账以及 Permissions.* 进行识别。已确立的形式在 1.x → 2.x → 3.x 边界之间保持兼容,同时扫描器也接受新记录的装饰器和底层转账变体。 2.x 的所有者认证惯用法 this.sender.getAndRequireSignature() 被识别为签名门控。(旧式 前置条件仍被接受,因此较旧的代码也不会被破坏。)

Mesa 的破坏性变更都是运行时和协议级别的——移除 Transaction.setFeePerSnarkCost() 和 TransactionCost.* 常量、新的 VerificationKey.toJSON() 形态、重新生成的验证密钥、MAX_ZKAPP_STATE_FIELDS 从 8 提升到 32,以及 mina-signer v4 交易格式。它们都没有重命名本扫描器所匹配的 API,因此 Mesa 没有规则变更,并且这是经过验证而非断言的。 scripts/o1js_release_matrix.sh 扫描两个跨越协议边界的固定 o1js 版本——2.15.0(9620ef08,最后一个 2.x 版本)和 3.0.0(cc18a919,Mesa)——并将每个发现结果与 tests/fixtures/o1js_release_matrix.json进行比较:

33 个发现结果在边界两侧完全相同,没有丢失,且所有三个新增发现结果都在 src/examples/zkapps/big-state-zkapp.ts 中——这个 32 状态字段示例之所以存在,仅仅是因为 Mesa 提升了 MAX_ZKAPP_STATE_FIELDS。该差异由测试固定,因此不会静默漂移。该矩阵在每次 CI 构建时运行;每周的 o1js-upstream-canary 任务还会额外跟踪 HEAD 处的 o1js,领先于任何发布。

等效的约束写法在分析时会被规范化:实例 assertEquals(...)、静态 Provable.assertEqual(Type, ...) 以及 equals(...).assertTrue() 相等链都绑定相同的操作数。方法提取在保持长度的注释和字符串掩码之后进行花括号平衡,并接受多行装饰器、嵌套回调形参数类型、TypeScript 访问修饰符以及多行身份别名(包括带括号和 as Type 形式)。

Noir 分析针对 Aztec / nargo 项目使用的 Noir 语法(.nr);它不调用 nargo 或编译电路。

工作原理

它是一个词法分析器,而非完整的 TypeScript 或 Noir 解析器——o1js 和 Noir 源代码以花括号分隔且可用正则处理,输出旨在由人工分类。这使其无依赖且在 CI 中即时运行。 发现结果是审查的起点,而非证明。

路线图 / 贡献

欢迎贡献——新的规则族、更多误报防护以及真实世界校准原型都很有价值。参见 CONTRIBUTING.md。

关于从 Community Packages 列表到 o1js 仓库中咨询检查的拟议路径,请参见可直接发送的 o1js 上游集成提案。

使用以下命令运行测试和 linter:```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only

root@kitploit:~
## 许可证

Apache-2.0。参见 [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE)。
下载工具
mod test { … }
mod tests { … }

发送者认证是基于名称且仅限同一方法。 O1JS_UNCONSTRAINED_SENDER 在 this.sender.getAndRequireSignature() 或 AccountUpdate.createSigned(<that sender>) 出现在同一 @method 主体中时进行抑制。仅存在于辅助函数中的签名要求(this.requireSenderSig() → 内部的 getAndRequireSignature)不会被追踪——失败模式是对包装了该惯用法的正确代码产生误报,而非遗漏真实缺陷。

  • Noir 跨 crate 辅助函数仅通过命名约定识别(无 Nargo.toml / 导入解析)。宁可漏报,不可误报。

  • assertEquals
    版本发现结果HIGHMEDIUMLOW文件
    o1js 2.15.036826218
    o1js 3.0.0 (Mesa)39829219