
o1js/Mina zkApps 및 Noir 회로의 zk 회로 건전성 버그를 위한 종속성 없는 정적 분석기
커뮤니티 패키지:
o1js-scan은 공식 o1js Community Packages 디렉터리에 등록되어 있습니다.
최신 버전: 0.20.0 — 이제 분석기가
extends TokenContract를 사용하는 컨트랙트를 인식합니다. 이 릴리스 이전까지 컨트랙트 게이트는SmartContract만 매칭했기 때문에, 생태계의 모든 대체 가능 토큰, NFT 컬렉션, AMM 풀이 "no findings"로 스캔되었습니다. 0.20.0 이전에 토큰 컨트랙트를 스캔했다면 다시 스캔하세요. CHANGELOG를 참고하세요.
다음에 대한 zk 회로 건전성 버그를 찾아내는 빠르고 의존성 없는 정적 분석기입니다:
.ts / .js) — @method 본문에서 생성되는 Kimchi 회로.nr) — Aztec의 Rust 스타일 ZK DSL (aztec-nr 형태의 패턴 포함)보안에 치명적인 버그는 보통 증명 시스템에 있는 것이 아니라
애플리케이션 자체의 제약 조건에 있습니다: 증명자가 제어하지만 회로가
결코 바인딩하지 않는 witness 말입니다. o1js-scan은 Mina와 Noir 생태계에서
Circom의 사촌 격인 언어들을 위한 under-constrained-signal 스캐너입니다.```bash
pip install 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
### 예시
`withdraw` 금액이 온체인 상태에 전혀 바인딩되지 않은
증명자 제어 witness인 vault가 주어졌을 때:```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
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
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 .
서드파티 Python 의존성이 없습니다. Python 3.8 이상. `noir-scan` 콘솔 스크립트는 npm 래퍼를 포함하여 `o1js-scan`(동일한 진입점)과 함께 설치됩니다.
## 사용법```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
Exit code는 --fail-on 레벨(기본값 high) 이상의 발견 사항이 있을 때 1, 그렇지 않으면 0이므로 CI에 바로 적용할 수 있습니다. 기본값에서는 low/medium 발견 사항(아래의 정보성 recipient 규칙 포함)이 빌드를 실패시키지 않습니다. --fail-on none을 사용하면 보고만 하고, --strict(--fail-on medium의 축약형)를 사용하면 low 심각도 발견 사항은 여전히 참고 사항으로 취급하면서 더 엄격하게 게이트를 적용할 수 있습니다. 두 옵션은 상호 배타적이므로 CI 구성이 모호해질 수 없습니다. 스캔 경로가 없으면 stderr에 오류를 출력하고 exit 2로 종료되므로, 오타가 조용히 CI를 깨끗한 실행으로 통과시키지 않습니다. 모든 실행은 stderr에 한 줄 요약(심각도별 개수와 게이트 판정)을 출력합니다.
테스트 코드는 기본적으로 제외됩니다 — 두 백엔드 모두. 테스트는 어설션이 이를 거부하는지 증명하기 위해 의도적으로 유효하지 않은 값과 잘못된 트랜잭션을 만들기 때문에, 그곳에서의 발견 사항은 회로 버그가 아니라 테스트의 목적 자체입니다. 다음에 해당하는 파일은 테스트 코드로 간주됩니다:
*.test.ts / *.spec.ts(및 .js/.jsx/.tsx/.mjs/.cjs 변형) 또는 *_test.nr / test_*.nr과 일치하는 경우;test/, tests/, __tests__/, spec/ 또는 __mocks__/ 디렉터리 아래에 있는 경우;#[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 });
| `--no-color` | Disable colored output |
| `--debug` | Enable debug mode |
| `--verbose` | Enable verbose mode |
| `--version` | Show version and exit |
| `--help` | Show help message and exit |
### Examples
```bash
# Basic scan
python3 cve_2025_55182.py -u https://target.example.com
# Scan with custom timeout and verbose output
python3 cve_2025_55182.py -u https://target.example.com -t 30 -v
# Scan multiple targets from file
python3 cve_2025_55182.py -f targets.txt -o results.json
# Scan with proxy
python3 cve_2025_55182.py -u https://target.example.com -p http://127.0.0.1:8080
# Scan with custom headers
python3 cve_2025_55182.py -u https://target.example.com -H "Authorization: Bearer token123"
The tool outputs results in JSON format by default:
{
"target": "https://target.example.com",
"vulnerable": true,
"cve": "CVE-2025-55182",
"timestamp": "2025-01-15T10:30:00Z",
"details": {
"endpoint": "/api/v1/endpoint",
"method": "POST",
"payload": "...",
"response_code": 200,
"response_time": 0.523
}
}
The scanner performs the following checks:
200 with specific response body patternsIssue: ModuleNotFoundError: No module named 'requests'
Solution: Install the required dependencies:
pip install -r requirements.txt
Issue: Connection timeout errors
Solution: Increase the timeout value using the -t flag:
python3 cve_2025_55182.py -u https://target.example.com -t 60
Issue: SSL certificate verification errors
Solution: Use the --no-verify flag to skip SSL verification (not recommended for production):
python3 cve_2025_55182.py -u https://target.example.com --no-verify
Contributions are welcome! Please follow these guidelines:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.
This tool is provided for educational and authorized security testing purposes only. The authors are not responsible for any misuse or damage caused by this tool. Always obtain proper authorization before testing any system.
For questions or concerns, please open an issue on GitHub or contact the maintainer at [email protected].```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)
몇 줄로 스캐너를 CI에 추가하세요. 발견 사항은 PR diff에 주석으로, 그리고 저장소의 Security → Code scanning 탭에 경고로 표시됩니다.```yaml
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
또는 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"]
Inputs: path (기본값 .), lang (auto|o1js|noir, 기본값 auto),
version (설치할 PyPI 버전, 기본값 최신), upload-sarif (기본값
true), fail-on (critical|high|medium|low|none, 기본값 ),
(더 이상 사용되지 않음, 기본값 ), (기본값
), (기본값 ). 출력: . SARIF
업로드에는 와 코드 스캐닝 활성화가 필요합니다.
리포트와 게이트는 하나의 인자 배열로부터 생성되므로, include-tests와
include-examples는 둘 모두에 적용됩니다 — 읽는 SARIF와 게이트하는 종료 코드는
항상 동일한 소스 집합을 설명합니다. 리포팅 패스는
--fail-on none으로 실행되어 발견 사항이 SARIF 업로드를 차단하지 않지만, 운영상의
실패(존재하지 않는 경로, CLI 사용 오류)는 여전히 해당 단계를 실패시킵니다.
깨끗한 스캔으로 보고되지 않습니다.
fail-on-findings: true는 호환성을 위해 유지되며 fail-on이 none으로
남아 있을 때 fail-on: high로 매핑됩니다. 사용 중단 경고를 발생시킵니다.
모든 심각도에서 게이트할 수 있는 fail-on을 사용하는 것이 좋습니다.
카운트는 각 백엔드가 지원하는 고유한 규칙 ID입니다. 문맥에 따라 심각도를 할당하는 규칙(예: 값 전송에는 high, 상태 쓰기에는 medium)은 둘 이상의 심각도 열에 나타나므로, 심각도 열은 의도적으로 규칙 합계와 일치하지 않습니다. 현재 critical 또는 info 심각도 규칙은 없습니다. 전체 설명과 오탐 방지 가드는 아래에 이어집니다.
분석기는 올바른 코드에서는 조용히 유지되도록 설계되었습니다:
this.requireSignature()(또는 getAndRequireSignature, AccountUpdate.createSigned,
Signature.verify)를 호출하는 @method는 소유자/관리자 게이트가 적용됩니다 — 그 인자는 임의의 증명자가 아니라 키
소유자가 선택하므로, 그 위트니스는 표시되지 않습니다. 이는
onlyOwner의 o1js 등가물입니다.getAndRequireEquals()에서 파생된
값과 동일하다고 검증되거나(또는 순서 비교로 범위가 제한된) 인자는 건전하며 보고되지 않습니다. 이는 직접 형태 —
amount.assertLessThanOrEqual(bal) — 와 연쇄 형태
amount.lessThanOrEqual(bal).assertTrue()를 모두 커버합니다. 데코레이터가 없는 동일 클래스 헬퍼(this.verifyX(arg))에 있는 바인딩도 인식되며,
그러한 헬퍼의 연쇄를 통해서도 마찬가지입니다..verify()가 호출된 Proof / / /
타입 인자는 검증된 서킷에 의해 제약됩니다 — 그에 대한 위트니스 발견 사항(및 그 /
)은 억제됩니다. 는 조건이 제약되지 않은 메서드 인자가 아니거나
그 자체로 검증된 경우에만 인정됩니다. 정규 OffchainState 래퍼
에도 동일하게 적용됩니다(프레임워크가 내부에서 검증).
직접 만든 는
검증한다고 . 반대 경우(proof 타입 인자가 결코 검증되지 않고 OffchainState로 settle되지도 않음)는 로 보고됩니다.동일한 건전성 개념 — 제약이 부족한 위트니스 — 이
Noir (.nr) 서킷에 적용됩니다. 스캐너를 .nr
파일로 향하게 하거나(--lang noir 사용) Noir 규칙 세트로 분석하게 하세요.
동일한 어휘적, 의존성 없는 접근 방식입니다. aztec-nr oracle /
unsafe 관용구에 맞춰 보정됨 — docs/noir_calibration.md 참조.
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나 형식 증명 계층이 없습니다.
트리아지할 때 이러한 사각지대를 염두에 두십시오 — 이는 이 의존성 없는 설계에서 알려져 있고 의도된 것이지 버그가 아닙니다:
- **단순 별칭만 추적됩니다.** Witness 추적은 `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
교차 메서드 바인딩은 동일 클래스 헬퍼 체인만을 다룹니다. this.verifyX(arg)로 호출되는 데코레이터 없는 동일 클래스 헬퍼는 호출자의 인자를 상태 바인딩할 수 있으며, 0.19.0 이후로는 그러한 헬퍼들의 체인(@method → 헬퍼 A → 헬퍼 B)이 고정점까지 추적됩니다. 헬퍼→헬퍼 단계는 순수 매개변수 참조만 매핑하므로 helperA(x.add(1))는 전파되지 않습니다. 자유 함수와 임포트된 함수는 여전히 추적되지 않으며, 헬퍼 인자의 지역 변수 앨리어싱은 문서화된 한계로 남아 있습니다.
미검증 Bool 탐지는 문장 형태를 따릅니다. Tier A는 가장 바깥쪽 호출이 Bool 술어이고 그 뒤에 아무것도 체인되지 않은 순수 표현식 문만을 플래그합니다. Provable.if(...) 내부에 중첩된 술어나 할당되어 나중에 사용되는 술어는 플래그되지 않습니다. Bool 지역 변수의 복잡한 제어 흐름 사용은 이름이 전혀 참조되지 않으면 여전히 놓칠 수 있습니다(실패 모드: 미탐, 오탐 아님).
시그니처 게이팅은 메서드 수준이며 부분 문자열 기반입니다. _method_is_signature_gated는 전체 @method가 시그니처 관용구를 포함하면 소유자 게이팅된 것으로 취급하며, 수신자 이름에 문자 그대로 signature가 포함된 경우에만 검증자를 인식합니다 — 따라서 sig.verify(admin, msg)는 게이팅으로 인식되지 않으며, 큰 메서드 내 다른 곳의 무관한 시그니처 검사는 과잉 억제할 수 있습니다. 메서드별로 전부 아니면 전무입니다.
발신자 인증은 이름 기반이며 동일 메서드 내에서만 적용됩니다. O1JS_UNCONSTRAINED_SENDER는 this.sender.getAndRequireSignature() 또는 가 본문에 나타날 때 억제합니다. 헬퍼에만 존재하는 시그니처 요구사항( → 내부의 )은 — 실패 모드는 관용구를 감싼 올바른 코드에 대한 오탐이며, 실제 버그를 놓치는 것이 아닙니다.
이것들이 발견 사항이 증명이 아니라 인간 검토의 출발점인 이유입니다. 데이터플로우를 인식하는 재작성은 어휘 분석기의 범위 밖으로 의도적으로 두었습니다.
o1js-scan은 의도적으로 얕은 단일 파일 어휘 패스입니다 — 파서도, 데이터플로우도, 솔버도 없습니다. 이것이 의존성 없이 CI에서 즉시 실행되게 하는 이유이며, 동시에 단단한 상한이기도 합니다. 위의 한계들은 백로그가 아니라 설계의 귀결입니다.
따라서 이 도구가 무엇을 알려줄 수 있고 무엇을 알려줄 수 없는지 명확히 하는 것이 가치 있습니다:
이 트레이드오프는 모든 커밋마다 실행하는 린터에 적합한 선택입니다. 차이가 중요한 작업 — 실제 가치를 보유한 프로토콜, 잘못되면 안 되는 회로 — 을 하고 있다면, 이것을 첫 번째 패스로 취급하고 실제 검토를 위한 예산을 잡으십시오.
더 깊은 분석을 위해, 별도의 전체 스캐너가 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에 있습니다 — Mesa가 MAX_ZKAPP_STATE_FIELDS를 상향했기 때문에만 존재하는 32개 상태 필드 예제입니다. 그 델타는 테스트로 고정되어 있어 조용히 드리프트할 수 없습니다. 매트릭스는 모든 CI 빌드에서 실행되며, 주간 o1js-upstream-canary 작업은 추가로 어떤 릴리스보다 앞서 o1js를 HEAD에서 추적합니다.
동등한 제약 표기는 분석을 위해 정규화됩니다: 인스턴스 assertEquals(...), 정적 Provable.assertEqual(Type, ...), 그리고 equals(...).assertTrue() 동등성 체인은 모두 동일한 피연산자를 바인딩합니다. 메서드 추출은 길이 보존 주석 및 문자열 마스킹 후 중괄호 균형을 맞추며, 여러 줄 데코레이터, 중첩된 콜백 형태 매개변수 타입, TypeScript 접근 제한자, 그리고 여러 줄 아이덴티티 앨리어스(괄호 및 as Type 형태 포함)를 허용합니다.
Noir 분석은 Aztec / nargo 프로젝트가 사용하는 Noir 구문(.nr)을 대상으로 합니다; nargo를 호출하거나 회로를 컴파일하지 않습니다.
이것은 전체 TypeScript 또는 Noir 파서가 아닌 어휘 분석기입니다 — o1js와 Noir 소스는 중괄호로 구분되고 정규식으로 다루기 쉬우며, 출력은 인간이 분류하도록 되어 있습니다. 이것이 의존성 없이 CI에서 즉시 실행되게 합니다. 발견 사항은 검토의 출발점이지 증명이 아닙니다.
기여를 환영합니다 — 새로운 규칙 패밀리, 더 많은 FP 가드, 실제 세계 캘리브레이션 원형 모두 가치 있습니다. CONTRIBUTING.md를 참조하십시오.
Community Packages 목록에서 o1js 저장소의 권고 검사로 가는 제안된 경로는 바로 보낼 수 있는 o1js 업스트림 통합 제안을 참조하십시오.
테스트와 린터를 다음으로 실행하십시오:```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only
## 라이선스
Apache-2.0. [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE)를 참조하세요.
mod test { … }mod tests { … }nonefail-on-findingsfalseinclude-testsfalseinclude-examplesfalsesarif-filesecurity-events: write| 백엔드 | 규칙 | High 지원 | Medium 지원 | Low 지원 |
|---|
| o1js | 18 | 11 | 12 | 2 |
| Noir | 11 | 4 | 9 | 1 |
| 합계 | 29 | 15 | 21 | 3 |
| 규칙 | 심각도 | 의미 |
|---|
O1JS_MISSING_STATE_PRECONDITION | high | 일치하는 requireEquals(...) / getAndRequireEquals() 없이 this.x.get()을 읽음. 단순한 get()은 계정 사전 조건을 전혀 추가하지 않으므로, 증명이 x를 온체인 값에 바인딩하지 않습니다 — 증명자는 임의의 값을 대체할 수 있습니다. |
O1JS_UNCONSTRAINED_WITNESS | high / medium | @method 인자(증명자가 제어하는 프라이빗 위트니스)가 전송 금액(this.send(...) 또는 동일 메서드의 AccountUpdate.create*(...).send(...))이나 상태 .set(...)으로 흘러들어가며 결코 검증되지 않음. 제약이 부족한 Circom 신호의 직접적인 유사 사례. 값 전송에 도달하면 high. |
O1JS_UNCONSTRAINED_PROVABLE_WITNESS | high / medium / low | Provable.witness(...) 로컬이 서킷 내 검증 없이 전송/상태 효과로 흘러들어감. 위트니스 콜백은 서킷 외부에서 실행되므로(단지 증명자 힌트일 뿐), 결과는 증명자가 제어하는 새로운 값입니다 — @method 인자 외의 또 다른 위트니스 소스입니다. 재도출하여 검증(x.assertEquals(<recomputed>))하거나 상태에 바인딩해야 합니다. 전송 금액(this.send(...) 또는 동일 메서드의 AccountUpdate.create*)에서는 high. |
O1JS_UNCONSTRAINED_RECIPIENT | low | @method 인자가 전송의 to: 수신자로만 사용됨. 이는 보통 의도된 것(사용자가 자신의 출금 대상을 지정)이며 정보 제공용입니다 — 대상이 고정된 treasury나 상태에 기록된 주소여야 하는 경우에만 중요합니다. CI 종료 코드 게이트를 트리거하지 않습니다. |
O1JS_WITNESS_NOT_BOUND_TO_STATE | medium | 위트니스가 효과 이전에 사소하게만 제약됨(예: > 0, 또는 상수와 비교) — 온체인 상태에 결코 연결되지 않음. 오프체인 오케스트레이션이 이를 안전하게 만드는지 확인하거나, 잔액이 현재 값까지 소진될 수 있습니다. |
O1JS_STALE_MERKLE_ROOT | high | 메서드가 증명자가 제공한 위트니스로부터 Merkle 루트를 재계산(computeRootAndKey / calculateRoot)하지만 재계산된 루트 중 어느 것도 현재 온체인 루트에 바인딩하지 않음. 라이브 루트에 대한 this.root.requireEquals(...) / assertEquals 없이는 증명자가 조작되거나 오래된 트리에 대한 위트니스를 전달할 수 있습니다 — 멤버십을 위조하거나 이전 상태를 재생합니다. 바인딩은 데코레이터가 없는 동일 클래스 헬퍼(this.verifyX(witness))에 있을 수 있으며, 헬퍼 전파가 이를 커버합니다. |
O1JS_UNVERIFIED_PROOF | high | Proof<...> / SelfProof<...> / DynamicProof<...> 타입의 @method 매개변수가 퍼블릭 필드가 사용되기 전에 .verify()되지 않음. Proof를 전달한다고 해서 검증되는 것은 아닙니다 — 명시적 검증 없이는 증명자가 임의의 proof 객체를 제공할 수 있으며, 그 publicOutput의 모든 사용은 제약되지 않습니다. .verifyIf(flag)가 제약되지 않은 @method 인자에 의해 게이트되고 proof의 퍼블릭 필드가 읽히는 경우에도 발생합니다. 증명자가 조건을 false로 만들 수 있기 때문입니다. |
O1JS_UNASSERTED_BOOL | high / medium | o1js 술어(equals / lessThanOrEqual / …)는 Bool을 반환하며 결과가 검증되거나 사용되지 않으면 어떤 제약도 추가하지 않음. 호출이 그냥 버려진 문장일 때 HIGH; 다시 참조되지 않는 로컬에 할당될 때 MEDIUM. |
O1JS_UNCONSTRAINED_SENDER | high / medium | this.sender.getUnconstrained()는 증명 없이 tx 발신자를 반환함. 그 값(또는 그로부터 파생된 로컬)이 assert / 상태 .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는 mod 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 | Provable.asProver(...) 또는 Provable.witness* 콜백 내부의 보안 로직(assert / approve / send / 상태 .set). 이러한 콜백은 서킷 외부에서 실행됩니다 — 악의적인 증명자가 이를 삭제하고도 검증되는 proof를 생성할 수 있습니다. |
O1JS_APPROVE_WITHOUT_BINDING | medium | @method가 balanceChange / publicKey를 읽지 않고 assertCanMint / assertCanBurn / forEachUpdate 보존 검사 없이 approve / approveAccountUpdate / approveBase를 호출함 — Mina FlawedTokenContract 원형. |
O1JS_VACUOUS_ASSERT | high / medium | 구조적으로 만족되는 assert: x.assertEquals(x), x.equals(x).assertTrue(), 또는 Bool(true).assertTrue(). 자기 비교에는 HIGH(거의 항상 오타); 상수 Bool assert에는 MEDIUM. |
O1JS_CONDITIONAL_ASSERT | medium | if <flag> { ... } 내부의 assert로, <flag>가 증명자가 제어하는 @method Bool(또는 .toBoolean()에서 파생된 로컬)인 경우. JS 조건문은 Provable.if처럼 서킷을 제약하지 않습니다. 정밀도를 위해 인라인 비교는 보고되지 않습니다. |
O1JS_GUARDED_INVERSE | medium | Provable.if 분기 내부의 .div() / .inv() / .sqrt()로, 실패하는 바로 그 값에 대한 조건으로 가드됨. 두 분기 모두 서킷 내에서 평가되며 이 호출들은 역수나 근이 존재한다고 무조건 assert하므로, 가드가 assert를 건너뛰지 않습니다 — 서킷은 가드가 처리하도록 작성된 바로 그 입력에 대해 만족 불가능하며, 메서드는 그 입력에 대해 결코 증명될 수 없습니다. Veridise에 의해 V-O1J-VUL-060으로 보고됨. 먼저 안전한 제수를 계산하고(Provable.if(isZero, Field(1), d)) 이후에 결과를 선택하세요. 다음의 경우 조용히 유지됨: 가드가 제수에 대해 아무 말도 하지 않아, 안전한 나눗셈을 둘러싼 무관한 Provable.if는 표시되지 않습니다. |
O1JS_PRECONDITION_OVERWRITTEN | medium | 한 메서드 내에서 동일한 속성에 대한 둘 이상의 requireEquals / requireBetween / requireNothing 호출로, 인자가 다른 경우. 사전 조건은 누적되는 것이 아니라 AccountUpdate에 설정되므로, 각 호출이 이전 것을 덮어쓰고 마지막 것만 강제됩니다 — 합성되는 서킷 내 assert와는 다릅니다. a.requireEquals(b) 다음 a.requireEquals(c)는 a === b가 아니라 a === c를 의미합니다. 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으로 보고됨. 상태를 다시 읽는 대신 새 값을 로컬에 유지하세요. 다음의 경우 조용히 유지됨: 읽기가 쓰기 자체의 인자 내부에 중첩된 경우(올바른 read-modify-write 관용구 this.x.set(this.x.getAndRequireEquals().add(1))), 그리고 쓰기와 읽기가 상호 배타적인 JS 분기에 있는 경우. 단일 메서드로 범위가 제한됨 — Veridise가 설명하는 교차 메서드 캐싱 사례는 이 규칙이 가지지 않은 호출 그래프 지식을 필요로 합니다. |
SelfProofDynamicProof*ProofpublicOutputpublicInput.verifyIf(flag)this.offchainState.settle(proof)settle.settle(proof)O1JS_UNVERIFIED_PROOF.assertTrue() / .assertFalse()로 연쇄되거나, Provable.if(...)에 중첩되거나,
나중에 참조되는 로컬에 할당된 술어는
O1JS_UNASSERTED_BOOL로 보고되지 않습니다.this.sender.getUnconstrained()는
동일한 @method가 this.sender.getAndRequireSignature()도 호출하거나, 그 위트니스된 값이
AccountUpdate.createSigned(...)에 전달되거나 그것으로부터 구축된 AccountUpdate에서
.requireSignature()로 인증된 경우(인자 동일성 필요) 발생하지 않습니다.assert가
잘못된 결과를 만들 수 없습니다.| 규칙 | 심각도 | 의미 |
|---|
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인 이유는 의도적으로 사용되지 않는 퍼블릭 입력이 proof를 컨텍스트(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의 암시적 범위 검사는 인덱스가 범위 내에 있다는 것만 확립할 뿐 — 올바른 인덱스인지는 확립하지 않으므로, 증명자는 임의의 원소를 선택하고도 검증되는 proof를 생성할 수 있습니다. 이것이 Merkle 경로 위치, note 선택, 허용 목록 멤버십 뒤에 있는 선택자 자유 버그입니다. 인덱스가 범위 제한되거나, 동등성으로 고정되거나, 캐스트 전에 범위 제한되거나(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 | 증명자가 제어하는 if 아래에서만 constrain_* / confirm_* / verify_* 호출이 이루어지면서, 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는 표시되지 않음 — 이는 만족 불가능하며, 조용한 건전성 구멍이 아니라 라이브니스 버그입니다. |
NOIR_UNSAFE_MISSING_SAFETY | low | 인접한 // Safety: 주석이 없는 unsafe { ... } 블록. 정보 제공용; 기본 --fail-on high에서 CI를 실패시키지 않음. |
AccountUpdate.createSigned(<해당 발신자>)@methodthis.requireSenderSig()getAndRequireSignatureNoir 크레이트 간 헬퍼는 이름 규칙으로만 인식됩니다(Nargo.toml / 임포트 해석 없음). 오탐보다 미탐을 선호합니다.
assertEquals| 릴리스 | 발견 사항 | HIGH | MEDIUM | LOW | 파일 |
|---|
| o1js 2.15.0 | 36 | 8 | 26 | 2 | 18 |
| o1js 3.0.0 (Mesa) | 39 | 8 | 29 | 2 | 19 |