
Analizzatore statico senza dipendenze per bug di solidità dei circuiti zk in o1js/Mina zkApps e circuiti Noir
Pacchetto della community:
o1js-scanè elencato nella directory ufficiale o1js Community Packages.
Ultima versione: 0.20.0 — l'analizzatore ora legge i contratti che
extends TokenContract. Fino a questa release il gate dei contratti corrispondeva solo aSmartContract, quindi ogni token fungibile, collezione NFT e pool AMM dell'ecosistema veniva scansionato come "nessun risultato". Se hai scansionato un contratto token prima della 0.20.0, scansiona di nuovo. Vedi .
Un analizzatore statico veloce e senza dipendenze per bug di soundness dei circuiti zk in:
.ts / .js) — circuiti Kimchi dai corpi dei @method.nr) — il DSL ZK in stile Rust di Aztec (inclusi pattern con struttura aztec-nr)I bug critici per la sicurezza di solito non sono nel sistema di proving — sono nei
vincoli dell'applicazione stessa: witness che il prover controlla ma che il circuito
non vincola mai. o1js-scan è lo scanner per segnali sotto-vincolati per i cugini di Circom
negli ecosistemi Mina e Noir.```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
### Esempio
Dato un vault il cui importo di `withdraw` è un witness controllato dal prover che
non è mai vincolato allo stato on-chain:```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 è necessario qui solo perché il file demo si trova sotto
examples/, che il classificatore di percorsi declassa per impostazione predefinita
affinché il codice di esempio di un repository non possa far fallire la sua build. Lo stesso
contratto nel tuo src/ segnala HIGH senza alcun flag.
Il risultato HIGH è il bug prosciugabile. Il contratto corretto
(examples/safe_vault.ts) lo elimina ed esce con 0, mantenendo solo il
LOW informativo sul destinatario scelto dal prover:```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
Consulta [`examples/`](https://github.com/auditinfra-io/o1js-scan/blob/main/examples) per le coppie vulnerabili/corrette di o1js e Noir.
## Contenuti
- [Installazione](#install)
- [Utilizzo](#usage) · [Sopprimere un risultato](#suppressing-a-reviewed-finding)
- [GitHub Action](#github-action)
- [Cosa rileva — o1js](#what-it-detects-o1js) · [Noir](#what-it-detects-noir)
- [Limitazioni note](#known-limitations) · [Dove si ferma questo strumento](#where-this-tool-stops)
- [Privacy e codice privato](#privacy-and-private-code)
- [Revisione post-quantum](#post-quantum-review)
- [Compatibilità](#compatibility) · [Come funziona](#how-it-works)
- [Contribuire](#roadmap--contributing)
## Installazione```bash
pip install o1js-scan
Per un'installazione CLI globale isolata, usa pipx:```bash
pipx install o1js-scan
Per i repository di app Noir, Aztec o o1js basati su Node/npm, installa il wrapper npm:```bash
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high
Il pacchetto npm è un sottile wrapper attorno allo stesso analizzatore Python e richiede
Python 3.8+ nel PATH (python3 o python). Imposta O1JS_SCAN_PYTHON per scegliere un
interprete specifico.
Oppure dai sorgenti:```bash git clone https://github.com/auditinfra-io/o1js-scan cd o1js-scan pip install -e .
Nessuna dipendenza Python di terze parti. Python 3.8+. Lo script console `noir-scan` è
installato insieme a `o1js-scan` (stesso entry point), anche tramite il wrapper
npm.
## Utilizzo```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
Il codice di uscita è 1 quando è presente un finding pari o superiore al livello --fail-on (predefinito high) e 0 altrimenti — così puoi inserirlo direttamente nella CI. Con il valore predefinito, un finding di livello low/medium (inclusa la regola informativa sul destinatario di seguito) non fa fallire la build; usa --fail-on none per limitarti a segnalare, oppure --strict (una scorciatoia per --fail-on medium) per applicare un gate più rigoroso trattando comunque i finding di bassa severità come advisory. Le due opzioni sono mutuamente esclusive, così la configurazione della CI non può essere ambigua. Un percorso di scansione mancante esce con 2 e un errore su stderr, così un refuso non può passare silenziosamente in CI come una scansione pulita. Ogni esecuzione stampa un riepilogo di una riga (conteggi per severità e verdetto del gate) su stderr.
Il codice di test è escluso per impostazione predefinita — su entrambi i backend. I test costruiscono deliberatamente valori non validi e transazioni errate per dimostrare che gli assert le rifiutano, quindi un finding lì è lo scopo del test, non un bug del circuito. Un file è considerato codice di test quando:
*.test.ts / *.spec.ts (e alle varianti .js/.jsx/.tsx/.mjs/.cjs), oppure a *_test.nr / test_*.nr;test/, tests/, __tests__/, spec/ o __mocks__/;#[test] / #[test(...)], oppure si trova all'interno di un blocco mod test { … } / mod tests { … } — con ambito di blocco, quindi un modulo di test in fondo a un file di produzione non silenzia il resto del file.Passa --include-tests per segnalarli.
Il codice di esempio viene declassato, non eliminato. Un finding in una directory examples/ o example/, o in un file chiamato *.eg.ts (anche .nr e le altre estensioni JS/TS), viene abbassato a LOW con una nota — ancora segnalato, ma non più in grado di far fallire una build. Il codice di esempio è deliberatamente semplificato, e segnalare gli esempi di un framework stesso come vulnerabilità è rumore; ma viene copiato in produzione molto più spesso del codice di test, ed è per questo che viene declassato anziché nascosto. Passa --include-examples per mantenere la severità originale.
Ogni volta che si applica una delle due politiche, l'esecuzione stampa una riga su stderr che lo indica — ad esempio 6 file(s) skipped as test code, 1 finding(s) downgraded as examples — così una scansione silenziosa non è mai silenziosamente tale. I conteggi compaiono anche nel SARIF sotto invocation.properties. Nota il compromesso: il rilevamento è basato solo sul percorso (nessun parsing di describe(/it(), quindi un circuito di produzione archiviato sotto tests/ verrà saltato — la riga su stderr è il modo in cui te ne accorgi.
Directory saltate durante l'attraversamento di un albero: node_modules, target (nargo), .git, dist, build, __pycache__, .venv, venv.
Silenzia un finding che hai già analizzato senza allentare il gate, con un commento inline sulla riga segnalata — o sulla riga sopra di essa:```ts this.send({ to, amount }); // o1js-scan-disable-line O1JS_UNCONSTRAINED_WITNESS
// o1js-scan-disable-next-line this.send({ to, amount });
| `-s` | `--server` | `SERVER` | `http://localhost:8080` | URL del server MCP |
| `-t` | `--token` | `TOKEN` | `None` | Token di autenticazione |
| `-k` | `--insecure` | flag | `False` | Disabilita la verifica del certificato TLS |
| `-v` | `--verbose` | flag | `False` | Abilita il logging dettagliato |
| `-o` | `--output` | `FILE` | `None` | Salva l'output su file |
| `-f` | `--format` | `FORMAT` | `text` | Formato di output (`text`, `json`) |
| `-c` | `--config` | `FILE` | `None` | File di configurazione |
| `-h` | `--help` | flag | - | Mostra il messaggio di aiuto |
### Esempi
```bash
# Elenca tutti gli strumenti disponibili
mcp-client --server http://localhost:8080 --list-tools
# Chiama uno strumento specifico
mcp-client --server http://localhost:8080 --call-tool scan_target --args '{"target": "example.com"}'
# Usa un file di configurazione
mcp-client --config ~/.mcp/config.json --list-tools
# Output in formato JSON
mcp-client --server http://localhost:8080 --list-tools --format json
Il client MCP supporta un file di configurazione JSON:
{
"server": "http://localhost:8080",
"token": "your-auth-token",
"insecure": false,
"verbose": false,
"timeout": 30,
"retries": 3
}
Posizionalo in ~/.mcp/config.json o specifica un percorso personalizzato con --config.
# Clona il repository
git clone https://github.com/example/mcp-client.git
cd mcp-client
# Crea un ambiente virtuale
python3 -m venv venv
source venv/bin/activate # Su Windows: venv\Scripts\activate
# Installa le dipendenze di sviluppo
pip install -r requirements-dev.txt
# Installa in modalità modificabile
pip install -e .
# Esegui tutti i test
pytest
# Esegui con copertura
pytest --cov=mcp_client --cov-report=html
# Esegui test specifici
pytest tests/test_client.py -v
# Esegui il linting del codice
ruff check .
# Formatta il codice
ruff format .
# Esegui il type checking
mypy mcp_client/
Authorization--insecure solo per lo sviluppo# Verifica che il server sia in esecuzione
curl http://localhost:8080/health
# Controlla la connettività di rete
ping localhost
# Verifica la configurazione del firewall
sudo ufw status
# Verifica che il token sia impostato
echo $MCP_TOKEN
# Testa l'autenticazione manualmente
curl -H "Authorization: Bearer $MCP_TOKEN" http://localhost:8080/tools
Aumenta il timeout nel file di configurazione o usa il flag --timeout:
mcp-client --server http://localhost:8080 --timeout 60 --list-tools
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)Questo progetto è rilasciato sotto la Licenza MIT - vedi il file LICENSE per i dettagli.
Nota: Questo strumento è destinato esclusivamente a test di sicurezza autorizzati e ricerca. Gli autori non sono responsabili per qualsiasi uso improprio o danno causato da questo software.```nr let inv = unsafe { hint(x) }; // o1js-scan-disable-line NOIR_UNCONSTRAINED_WITNESS
Elenca uno o più id di regole per sopprimere solo quelle; una direttiva nuda (senza id)
sopprime ogni regola sulla riga di destinazione.
Come libreria:```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)
Aggiungi lo scanner alla CI in poche righe. I risultati appaiono come annotazioni sul diff della PR e come avvisi nella scheda Security → Code scanning del repository.```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
### Ricetta CI solo per Noir
Consigliata per i progetti Noir che desiderano avvisi di scansione del codice e un gate per le vulnerabilità ad alta gravità:```yaml
- uses: auditinfra-io/[email protected]
with:
path: .
lang: noir
fail-on: high
O senza l'Action:```bash pip install o1js-scan noir-scan . --lang noir --fail-on high --sarif noir.sarif
### pre-commit (opzionale)```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"]
Input: path (predefinito .), lang (auto|o1js|noir, predefinito auto),
version (versione PyPI da installare, predefinito l'ultima), upload-sarif (predefinito
true), fail-on (critical|high|medium|low|none, predefinito none),
fail-on-findings (deprecato, predefinito false), include-tests (predefinito
false), include-examples (predefinito false). Output: sarif-file. Il caricamento
SARIF richiede security-events: write e il code scanning abilitato.
Il report e il gate sono costruiti da un unico array di argomenti, quindi include-tests
e include-examples si applicano a entrambi — il SARIF che leggi e il codice di uscita su
cui applichi il gate descrivono sempre lo stesso insieme di sorgenti. La passata di
reporting viene eseguita con --fail-on none in modo che i risultati non blocchino mai il
caricamento SARIF, ma un errore operativo (un percorso che non esiste, un errore d'uso
della CLI) fa comunque fallire lo step invece di essere segnalato come scansione pulita.
fail-on-findings: true è mantenuto per compatibilità e viene mappato a fail-on: high
quando fail-on è lasciato a none; emette un avviso di deprecazione. Preferisci
fail-on, che può applicare il gate a qualsiasi severità.
| Backend | Regole | High-capable | Medium-capable | Low-capable |
|---|---|---|---|---|
| o1js | 18 | 11 | 12 | 2 |
| Noir | 11 | 4 | 9 | 1 |
| Totale | 29 | 15 | 21 | 3 |
I conteggi sono ID di regole distinti supportati da ciascun backend. Una regola che assegna la severità in base al contesto (ad esempio, high per un trasferimento di valore e medium per una scrittura di stato) compare in più di una colonna di severità, quindi le colonne di severità intenzionalmente non sommano al totale delle regole. Attualmente non esistono regole di severità critical o info. Le descrizioni complete e le protezioni contro i falsi positivi seguono di seguito.
| Regola | Severità | Cosa significa |
|---|---|---|
O1JS_MISSING_STATE_PRECONDITION | high | this.x.get() letto senza un corrispondente requireEquals(...) / getAndRequireEquals(). Un get() nudo non aggiunge nessuna precondizione sull'account, quindi la proof non vincola x al suo valore on-chain — un prover può sostituire qualsiasi valore. |
O1JS_UNCONSTRAINED_WITNESS | high / medium | Un argomento di @method (un witness privato controllato dal prover) confluisce in un importo di invio (this.send(...) o un AccountUpdate.create*(...).send(...) dello stesso metodo) o in un .set(...) di stato e non viene mai asserito. Analogo diretto di un segnale Circom sotto-vincolato. High quando raggiunge un trasferimento di valore. |
O1JS_UNCONSTRAINED_PROVABLE_WITNESS | high / medium / low | Una variabile locale Provable.witness(...) confluisce in un effetto di invio/stato con nessuna asserzione nel circuito. Il callback del witness viene eseguito fuori dal circuito (è solo un suggerimento per il prover), quindi il risultato è un valore fresco controllato dal prover — l'altra fonte di witness oltre agli argomenti di @method. Deve essere ri-derivato e asserito (x.assertEquals(<recomputed>)) o vincolato allo stato. High su un importo di invio (this.send(...) o AccountUpdate.create* dello stesso metodo). |
O1JS_UNCONSTRAINED_RECIPIENT | low | Un argomento di @method è usato solo come destinatario to: di un invio. Di solito è intenzionale (un utente indica la propria destinazione di prelievo) ed è informativo — conta solo se la destinazione deve essere una tesoreria fissa o un indirizzo registrato nello stato. Non attiva il gate del codice di uscita della CI. |
O1JS_WITNESS_NOT_BOUND_TO_STATE | medium | Un witness è vincolato solo trivialmente (ad es. > 0, o confrontato con una costante) prima di un effetto — mai legato allo stato on-chain. Conferma che l'orchestrazione off-chain lo renda sicuro, altrimenti il saldo è drenabile fino al suo valore corrente. |
L'analizzatore è progettato per restare silenzioso su codice corretto:
@method che chiama
this.requireSignature() (o getAndRequireSignature, AccountUpdate.createSigned,
Signature.verify) è protetto da owner/admin — i suoi argomenti sono scelti dal
detentore della chiave, non da un prover arbitrario — quindi i suoi witness non vengono
segnalati. Questo è l'equivalente o1js di onlyOwner.getAndRequireEquals() è corretto e non verrà segnalato. Questo copre sia la forma
diretta — amount.assertLessThanOrEqual(bal) — sia la forma concatenata
amount.lessThanOrEqual(bal).assertTrue(). Anche il binding che risiede in un helper
non decorato della stessa classe (this.verifyX(arg)) è riconosciuto, anche attraverso
una catena di tali helper.Proof / SelfProof /
DynamicProof / *Proof su cui viene chiamato .verify() è vincolato dal circuito
verificato — i risultati sui witness su di esso (e sui suoi publicOutput /
publicInput) sono soppressi. Un .verifyIf(flag) è accreditato solo quando la
condizione non è un argomento di metodo non vincolato, o è essa stessa asserita. Lo
stesso vale per il wrapper canonico OffchainState this.offchainState.settle(proof)
(il framework verifica dentro settle). Un .settle(proof) scritto a mano non si
presume che verifichi. Il caso inverso (argomento tipizzato proof mai verificato e non
regolato da OffchainState) è segnalato come O1JS_UNVERIFIED_PROOF..assertTrue() / .assertFalse(), annidato in Provable.if(...), o assegnato a una
variabile locale successivamente referenziata, non è segnalato come
O1JS_UNASSERTED_BOOL.this.sender.getUnconstrained() non scatta
quando lo stesso @method chiama anche this.sender.getAndRequireSignature(), o
quando quel valore witness è passato a AccountUpdate.createSigned(...) / autenticato
tramite .requireSignature() su un AccountUpdate costruito da esso (è richiesta
l'identità dell'argomento).assert
dentro una stringa non può creare un risultato falso.La stessa idea di correttezza — witness sotto-vincolati — si applica ai circuiti
Noir (.nr). Punta lo scanner su file .nr (o usa
--lang noir) e li analizza con il set di regole Noir. Stesso approccio lessicale,
senza dipendenze. Calibrato sugli idiomi oracle / unsafe di aztec-nr — vedi
docs/noir_calibration.md.
| Regola | Severità | Cosa significa |
|---|---|---|
NOIR_UNCONSTRAINED_WITNESS | high | Un valore vincolato da un blocco unsafe { ... } — il risultato di una unconstrained fn (hint oracle / Brillig) — che non viene mai ri-vincolato da un assert / assert_eq (o un helper di conferma / controllo merkle). L'hint viene eseguito fuori dal circuito. Analogo di O1JS_UNCONSTRAINED_PROVABLE_WITNESS. |
NOIR_UNCONSTRAINED_INPUT | medium | Un input privato (witness) di fn main che non confluisce in nessun assert / assert_eq e non fa parte dell'output pubblico. Analogo di O1JS_UNCONSTRAINED_WITNESS. |
NOIR_UNCONSTRAINED_PUBLIC_INPUT | medium | Un input pubblico di fn main che non raggiunge alcun vincolo e nessun output — il circuito non lo legge mai. Il duale della regola sul witness privato: il verificatore fornisce il valore e crede che l'asserzione lo riguardi, mentre il circuito lo ignora (ad es. un merkle_root: pub Field che non viene mai controllato, quindi l'appartenenza non è mai stata realmente provata). MEDIUM perché un input pubblico deliberatamente inutilizzato è anche un idioma legittimo per vincolare una proof a un contesto (nonce / chain id / destinatario), che è lessicalmente indistinguibile — quindi non applica il gate della CI al valore predefinito --fail-on high. |
NOIR_UNCHECKED_CAST | medium | Un valore controllato dal prover convertito a un tipo unsigned stretto (as u8/u16/u32) con nessuna asserzione di intervallo. Analogo di MissingRangeCheck di o1js. |
NOIR_UNCONSTRAINED_ARRAY_INDEX | medium | Un valore controllato dal prover usato come indice di array (arr[i]) con nessun controllo di alcun tipo su di esso. Il controllo implicito dei limiti di Noir stabilisce solo che l'indice è nell'intervallo — non che sia l'indice corretto — quindi il prover resta libero di selezionare qualsiasi elemento e produrre comunque una proof che verifica. Questo è il bug della libertà di selezione dietro le posizioni dei percorsi di Merkle, la selezione delle note e l'appartenenza alle allow-list. Soppresso quando l'indice è limitato a un intervallo, fissato da un'uguaglianza, limitato prima di un cast (), o quando il valore riletto è esso stesso fissato da un . |
unsafe.constrain_* / confirm_* / verify_* /
check_(non_)membership* / public_data_storage_read accreditano gli argomenti (con
rilevamento dei risultati inutilizzati per i controlli scartati).// Safety: adiacente):
random(), avm::…, e la formulazione differita kernel/rollup/discovery.let su tupla + flag asseriti vincolano i witness merkle passati ai controlli di appartenenza.Esempio:```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)
Come con l'esempio o1js sopra, `--include-examples` è necessario solo perché
questi file demo si trovano sotto `examples/`.
## Limitazioni note
L'analizzatore è un **frontend lessicale senza dipendenze più un livello semantico leggero** che effettua il tracciamento degli alias e la propagazione interprocedurale attraverso helper della stessa classe. Non è un frontend del compilatore TypeScript, un type checker o un motore di dataflow sull'intero programma, e non c'è alcun livello SMT o di prova formale in questo scanner.
Tieni presenti questi punti ciechi durante la triage — sono noti e intenzionali
per questo design senza dipendenze, non bug:
- **Vengono seguiti solo gli alias semplici.** Il tracciamento dei witness segue
alias semplici dello stesso metodo come `const q = qty`, ma non espressioni
derivate o destrutturazione: ```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
Il binding cross-method copre solo catene di helper della stessa classe. Un
helper non decorato della stessa classe chiamato come this.verifyX(arg) può fare
state-binding dell'argomento di un chiamante, e dalla 0.19.0 le catene di questi
(@method → helper A → helper B) vengono seguite fino a un punto fisso. Il
passaggio helper→helper mappa solo un riferimento a parametro nudo, quindi
helperA(x.add(1)) non si propaga. Le funzioni libere e importate non vengono
ancora seguite, e l'aliasing tramite variabile locale dell'argomento dell'helper
resta una limitazione documentata.
Il rilevamento di Bool non asseriti è basato sulla forma dell'istruzione. Il Tier A segnala solo
istruzioni di espressione nude la cui chiamata più esterna è un predicato Bool senza nulla
concatenato dopo. I predicati annidati dentro Provable.if(...), o assegnati
e usati successivamente, non vengono segnalati. Usi complessi di controllo di flusso di un Bool locale
possono ancora sfuggire se il nome non viene mai referenziato (modalità di fallimento: mancata rilevazione,
non un falso positivo).
Il signature-gating è a livello di metodo e basato su sottostringhe.
_method_is_signature_gated tratta un intero @method come owner-gated se
contiene un idioma di firma, e riconosce un verificatore solo quando il
nome del receiver contiene letteralmente signature — quindi sig.verify(admin, msg)
non viene riconosciuto come gating, mentre un controllo di firma non correlato altrove
in un metodo grande può sopprimere eccessivamente. È tutto-o-niente per metodo.
L'autenticazione del sender è basata sul nome e solo nello stesso metodo.
O1JS_UNCONSTRAINED_SENDER sopprime quando this.sender.getAndRequireSignature()
o AccountUpdate.createSigned(<quel sender>) appare nello stesso corpo del @method. Un requisito di firma che risiede solo in un helper
(this.requireSenderSig() → getAndRequireSignature all'interno) non viene
seguito — la modalità di fallimento è un falso positivo su codice corretto che incapsula
l'idioma, non un bug reale mancato.
Gli helper cross-crate di Noir sono riconosciuti solo per convenzione di nome (nessuna
risoluzione di Nargo.toml / import). Preferire la mancata rilevazione al falso positivo.
Questi sono il motivo per cui i risultati sono un punto di partenza per la revisione umana, non prove. Una riscrittura consapevole del dataflow è deliberatamente fuori ambito per l'analizzatore lessicale.
o1js-scan è deliberatamente un passaggio lessicale superficiale su singolo file — nessun parser, nessun dataflow, nessun solver. È ciò che lo rende privo di dipendenze e istantaneo in CI, ed è anche un tetto invalicabile. Le limitazioni sopra non sono un backlog; sono conseguenze del design.
Quindi vale la pena essere espliciti su cosa questo strumento può e non può dirti:
Questo compromesso è quello giusto per un linter che esegui a ogni commit. Se stai lavorando a qualcosa dove la differenza conta — un protocollo che detiene valore reale, un circuito che non puoi permetterti di sbagliare — trattalo come il primo passaggio e metti in budget una revisione seria.
Per un'analisi più approfondita, lo scanner completo separato è mantenuto nel
repository audit-engine-cli.
o1js-scan è lo scanner intenzionalmente leggero e aperto; la conoscenza proprietaria
di rilevamento e i dettagli implementativi dello scanner completo non sono riprodotti
qui. Per accesso o una revisione del circuito più completa, contatta:
[email protected].
La CLI installata analizza i file localmente. Non ha telemetria, client di rete,
account o passaggio di upload, e il suo runtime Python non ha dipendenze di terze parti.
Eseguire o1js-scan path/to/private-repo non invia il sorgente
o i risultati da nessuna parte.
Come i log del compilatore, l'output dello scanner può contenere percorsi, identificatori e frammenti di sorgente. SARIF identifica anche posizioni esatte del repository, e la GitHub Action lo carica su GitHub code scanning. Usa gli stessi controlli di accesso al repository e alla CI che già usi per il sorgente scansionato.
Vuoi contribuire con un utile report di falso positivo o di mancata rilevazione senza condividere un'applicazione? Riproduci la sintassi con nomi e costanti inventati, rimuovi la logica di business una istruzione alla volta, e verifica che lo snippet sintetico attivi ancora la stessa regola prima di pubblicarlo. La guida ai contributi privacy-safe ha una checklist concreta e diversi modi per aiutare la comunità o1js senza divulgare un circuito privato.
Questo confine non impedisce allo scanner aperto di migliorare. La documentazione pubblica di o1js e i repository possono supportare nuove regole e fixture di compatibilità; esempi sintetici possono testare falsi positivi e vincoli mancati; e la resilienza del parser, la diagnostica, SARIF, le prestazioni, il packaging e la calibrazione possono tutti migliorare senza pubblicare una tecnica di audit privata o codice cliente. Lo scanner aperto dovrebbe fare affermazioni spiegabili in modo indipendente; la ricerca privata può rimanere nel motore di audit separato.
Il rischio quantistico è correlato alla sicurezza del circuito, ma non è una regola di
vincolo mancante. o1js-scan non determina se una firma, un hash, un commitment, il
sistema di proof Kimchi o Mina stessa soddisfi un obiettivo di sicurezza post-quantum. Quelle
risposte dipendono dalla primitiva e dai parametri concreti, dalle assunzioni della piattaforma,
dalla durata richiesta del deployment e dal suo piano di migrazione — non semplicemente da un
identificatore TypeScript che uno scanner lessicale può vedere.
Ispirata a Qubit or Not Qubit di O(1) Labs, la guida alla revisione post-quantum trasforma quel confine in un inventario specifico per o1js e una checklist di crypto-agility. Usala insieme a questo scanner invece di interpretare una scansione pulita come una valutazione post-quantum.
Funziona su o1js 1.x, 2.x e 3.x, incluso l'hard fork Mesa a cui punta o1js
3.0.0. o1js-scan analizza il sorgente TypeScript come testo e non ha alcuna dipendenza
a runtime da o1js — nulla è vincolato a una versione. Si basa sulla moderna
API di precondizioni require* (getAndRequireEquals, requireEquals,
requireSignature, getAndRequireSignature), sui
decoratori @method / @method() / @method.returns(...), sui campi @state
annotati, su this.send({...}), sui trasferimenti di basso livello AccountUpdate.balance.subInPlace(...)
e su Permissions.*. Le forme consolidate restano compatibili attraverso
i confini 1.x → 2.x → 3.x, mentre lo scanner accetta anche le varianti di decoratore
e di trasferimento di basso livello appena documentate.
L'idioma di owner-auth di 2.x this.sender.getAndRequireSignature() è riconosciuto
come signature-gating. (Le precondizioni legacy assertEquals sono ancora accettate,
quindi anche il codice più vecchio non si rompe.)
I cambiamenti di rottura di Mesa sono tutti a livello di runtime e protocollo — la rimozione di
Transaction.setFeePerSnarkCost() e delle costanti TransactionCost.*, la
nuova forma di VerificationKey.toJSON(), le verification key rigenerate,
MAX_ZKAPP_STATE_FIELDS aumentato da 8 a 32, e il formato di transazione
mina-signer v4. Nessuno di essi rinomina un'API su cui questo scanner fa match, quindi nessuna
regola è cambiata per Mesa, e ciò è verificato anziché asserito.
scripts/o1js_release_matrix.sh scansiona due release o1js fissate che stanno a cavallo
del confine di protocollo — 2.15.0 (9620ef08, l'ultima release 2.x) e
3.0.0 (cc18a919, Mesa) — e confronta ogni risultato con
tests/fixtures/o1js_release_matrix.json:
| Release | Findings | HIGH | MEDIUM | LOW | Files |
|---|---|---|---|---|---|
| o1js 2.15.0 | 36 | 8 | 26 | 2 | 18 |
| o1js 3.0.0 (Mesa) | 39 | 8 | 29 | 2 | 19 |
33 risultati sono identici attraverso il confine, nessuno è andato perso, e tutti e tre
quelli nuovi sono in src/examples/zkapps/big-state-zkapp.ts — l'esempio con 32 campi di stato
che esiste solo perché Mesa ha alzato MAX_ZKAPP_STATE_FIELDS. Quel
delta è fissato da un test, quindi non può andare alla deriva silenziosamente. La matrice viene eseguita a ogni
build CI; il job settimanale o1js-upstream-canary traccia inoltre o1js a
HEAD, in anticipo su qualsiasi release.
Le grafie equivalenti dei vincoli sono normalizzate per l'analisi: assertEquals(...)
di istanza, Provable.assertEqual(Type, ...) statico, e le catene di uguaglianza
equals(...).assertTrue() legano tutti gli stessi operandi. L'estrazione dei metodi
è brace-balanced dopo masking di commenti e stringhe a lunghezza preservata, e
accetta decoratori multilinea, tipi di parametro annidati a forma di callback,
modificatori di accesso TypeScript e alias di identità multilinea (incluse
le forme tra parentesi e as Type).
L'analisi Noir prende di mira la sintassi Noir usata dai progetti Aztec / nargo (.nr); non
invoca nargo né compila circuiti.
È un analizzatore lessicale, non un parser completo TypeScript o Noir — i sorgenti o1js e Noir sono delimitati da parentesi graffe e trattabili con regex, e l'output è pensato per essere valutato da un umano. Questo lo mantiene privo di dipendenze e istantaneo da eseguire in CI. I risultati sono un punto di partenza per la revisione, non prove.
Contributi benvenuti — nuove famiglie di regole, più guardie FP e archetipi
di calibrazione reali sono tutti preziosi. Vedi CONTRIBUTING.md.
Per un percorso proposto dalla voce Community Packages a un controllo advisory nel repository o1js, vedi la proposta di integrazione upstream o1js pronta da inviare.
Esegui i test e il linter con:```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only
## Licenza
Apache-2.0. Vedi [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE).
O1JS_STALE_MERKLE_ROOT | high | Un metodo ricalcola una radice di Merkle da un witness fornito dal prover (computeRootAndKey / calculateRoot) ma non vincola nessuna delle radici ricalcolate alla radice on-chain corrente. Senza un this.root.requireEquals(...) / assertEquals rispetto alla radice attiva, un prover può passare un witness per un albero fabbricato o obsoleto — falsificando l'appartenenza o riproducendo uno stato vecchio. Il binding può risiedere in un helper non decorato della stessa classe (this.verifyX(witness)); la propagazione degli helper copre questo caso. |
O1JS_UNVERIFIED_PROOF | high | Un parametro di @method tipizzato come Proof<...> / SelfProof<...> / DynamicProof<...> non viene mai sottoposto a .verify() prima che i suoi campi pubblici siano usati. Passare una Proof non la verifica — senza una verify esplicita il prover può fornire un oggetto proof arbitrario, e qualsiasi uso del suo publicOutput è non vincolato. Scatta anche quando .verifyIf(flag) è condizionato da un argomento di @method non vincolato e i campi pubblici della proof vengono letti, perché il prover può rendere la condizione falsa. |
O1JS_UNASSERTED_BOOL | high / medium | Un predicato o1js (equals / lessThanOrEqual / …) restituisce un Bool e non aggiunge nessun vincolo a meno che il risultato non sia asserito o usato. HIGH quando la chiamata è un'istruzione nuda scartata; MEDIUM quando è assegnata a una variabile locale mai più referenziata. |
O1JS_UNCONSTRAINED_SENDER | high / medium | this.sender.getUnconstrained() restituisce il mittente della tx senza provarlo. HIGH quando quel valore (o una variabile locale da esso derivata) confluisce in un assert / .set di stato / send (controllo vacuo); MEDIUM altrimenti. Preferisci this.sender.getAndRequireSignature(), o l'idioma esteso AccountUpdate.createSigned(sender). Resta silenzioso quando (1) lo stesso @method chiama anche this.sender.getAndRequireSignature() ovunque (il requisito di firma è a livello di metodo), oppure (2) il valore del mittente witness è l'argomento di AccountUpdate.createSigned(...) / un AccountUpdate.create(...).requireSignature() sulla stessa chiave (è richiesta l'identità dell'argomento — un createSigned su una chiave diversa non sopprime). |
MissingRangeCheck | high | Un Field grezzo (non il UInt64/UInt32 con controllo di intervallo) è usato come importo di trasferimento. Un Field è un elemento mod p e non è limitato a un intervallo. |
O1JS_WEAK_PERMISSIONS | high / medium | editState / send impostati a proofOrSignature() o none(), lasciando che la chiave dell'account zkApp aggiri il circuito firmando. Segnala anche setVerificationKey / setPermissions lasciati a signature / proofOrSignature / none (le ruote di addestramento per l'upgrade documentate da Mina); HIGH quando combinati con un editState/send debole nello stesso permissions.set. |
O1JS_LOGIC_OUTSIDE_PROOF | high | Logica di sicurezza (assert / approve / send / .set di stato) dentro Provable.asProver(...) o un callback Provable.witness*. Quei callback vengono eseguiti fuori dal circuito — un prover malevolo può eliminarli e produrre comunque una proof che verifica. |
O1JS_APPROVE_WITHOUT_BINDING | medium | Un @method chiama approve / approveAccountUpdate / approveBase senza leggere balanceChange / publicKey e senza assertCanMint / assertCanBurn / un controllo di conservazione forEachUpdate — l'archetipo Mina FlawedTokenContract. |
O1JS_VACUOUS_ASSERT | high / medium | Un assert soddisfatto per costruzione: x.assertEquals(x), x.equals(x).assertTrue(), o Bool(true).assertTrue(). HIGH per auto-confronti (quasi sempre un refuso); MEDIUM per assert su Bool costanti. |
O1JS_CONDITIONAL_ASSERT | medium | Un assert dentro if <flag> { ... } dove <flag> è un Bool di @method controllato dal prover (o una variabile locale da .toBoolean()). Un condizionale JS non vincola il circuito come fa Provable.if. I confronti inline restano non segnalati per precisione. |
O1JS_GUARDED_INVERSE | medium | Un .div() / .inv() / .sqrt() dentro un ramo di Provable.if, protetto da una condizione proprio sul valore su cui fallisce. Entrambi i rami sono valutati nel circuito e queste chiamate asseriscono incondizionatamente che l'inverso o la radice esista, quindi la guardia non salta l'asserzione — il circuito è insoddisfacibile esattamente per l'input che la guardia doveva gestire, e il metodo non può mai essere provato per esso. Segnalato da Veridise come V-O1J-VUL-060. Calcola prima un divisore sicuro (Provable.if(isZero, Field(1), d)) e seleziona il risultato dopo. Resta silenzioso quando la guardia non dice nulla sul divisore, quindi un Provable.if non correlato attorno a una divisione sicura non viene segnalato. |
O1JS_PRECONDITION_OVERWRITTEN | medium | Due o più chiamate requireEquals / requireBetween / requireNothing sulla stessa proprietà in un metodo, con argomenti diversi. Le precondizioni vengono impostate sull'AccountUpdate anziché accumulate, quindi ogni chiamata sovrascrive la precedente e solo l'ultima è applicata — a differenza delle asserzioni nel circuito, che si compongono. a.requireEquals(b) poi a.requireEquals(c) implica a === c, non a === b. Segnalato da Veridise come V-O1J-VUL-012. Resta silenzioso quando gli argomenti sono identici (idempotente, nulla perso), su getAndRequireEquals() (un metodo diverso, quindi letture ripetute dello stato vanno bene), e quando le chiamate si trovano in rami JS mutuamente esclusivi, che vengono risolti al momento della costruzione del circuito. Quest'ultima esenzione può nascondere una sovrascrittura reale che attraversa un if/else non correlato. |
O1JS_STATE_READ_AFTER_WRITE | medium | Un campo @state viene letto (get() / getAndRequireEquals()) dopo che un set(...) sullo stesso campo è completato, nello stesso metodo. set() registra la modifica sull'AccountUpdate ma non la propaga a get(), quindi la lettura osserva ancora il valore precedente alla scrittura e qualsiasi aritmetica costruita su di esso è silenziosamente sfasata di quella scrittura. Segnalato da Veridise come V-O1J-VUL-030. Mantieni il nuovo valore in una variabile locale invece di rileggere lo stato. Resta silenzioso quando la lettura è annidata negli argomenti stessi della scrittura (l'idioma read-modify-write this.x.set(this.x.getAndRequireEquals().add(1)), che è corretto), e quando la scrittura e la lettura si trovano in rami JS mutuamente esclusivi. Limitato a un singolo metodo — il caso di caching tra metodi che Veridise descrive richiede una conoscenza del call-graph che questa regola non possiede. |
index.assert_max_bit_size::<8>(); let i = index as u32;assert_eqNOIR_UNASSERTED_BOOL | high / medium | Un confronto il cui risultato bool viene scartato. Analogo di O1JS_UNASSERTED_BOOL di o1js. |
NOIR_CONDITIONAL_ASSERT | medium | Un assert dentro if <flag> { ... } dove <flag> è un bool nudo controllato dal prover o una variabile locale derivata da valori controllati dal prover. Un vincolo dentro un condizionale si applica solo quando la condizione è vera, quindi un ramo scelto dal prover può saltare il controllo. I confronti inline (if x != 0) vengono lasciati stare per precisione; assegnare la guardia a una variabile locale (let gate = x != 0; if gate) viene segnalato a meno che gate non sia esso stesso asserito. |
NOIR_CONDITIONAL_CONSTRAIN | medium | Una chiamata constrain_* / confirm_* / verify_* solo sotto un if controllato dal prover, mentre un hint unsafe raggiunge comunque l'output. |
NOIR_UNUSED_CHECK_RESULT | high / medium | Un risultato check_* / confirm_* / verify_* / constrain_* viene scartato (chiamata nuda) o assegnato e mai asserito — il controllo non vincola il circuito. |
NOIR_VACUOUS_CONSTRAINT | high / medium | Un vincolo soddisfatto per costruzione: un auto-confronto (assert(x == x), assert_eq(x, x), x >= x) o una condizione costante (assert(true)). Non aggiunge alcuna restrizione, ma la riga si legge come un controllo — il che la rende più pericolosa di un vincolo mancante, perché la revisione si ferma lì. HIGH per un auto-confronto (quasi sempre un refuso per un controllo reale: assert(computed == expected) scritto per errore come assert(expected == expected)); MEDIUM per una costante, che è più spesso un segnaposto. x != x non viene segnalato — è insoddisfacibile, un bug di liveness piuttosto che un buco di correttezza silenzioso. |
NOIR_UNSAFE_MISSING_SAFETY | low | Un blocco unsafe { ... } senza un commento // Safety: adiacente. Informativo; non fa fallire la CI al valore predefinito --fail-on high. |