Skip to content

V12 issues - #126

Merged
illuzen merged 74 commits into
mainfrom
illuzen/v12-2
Aug 5, 2026
Merged

V12 issues#126
illuzen merged 74 commits into
mainfrom
illuzen/v12-2

Conversation

@illuzen

@illuzen illuzen commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the V12 security audit findings in v12-issues.md.

Scope: only findings with Validity: Unreviewed. V12 marks 314 Low findings as Validity: Invalid (likely incorrect); those are excluded from this analysis and are not treated as open work.

Severity Unreviewed (in scope) Invalid (excluded)
High 20 0
Medium 30 0
Low 11 314
Info 2 0

This PR remediates all 20 High and 30 Medium Unreviewed findings (with red→green tests where applicable). The 11 Unreviewed Lows are listed below; most remain open for follow-up. A few Invalid Lows were hardened opportunistically and are noted separately (out of audit scope).

34 commits on illuzen/v12-2. Library suite: 211+ tests passing.

High (20/20 addressed)

ID Title Outcome
#159453 Wallet mnemonic/seed as CLI args Fixed — always hidden prompt
#159924 Malformed wallet aborts listing Fixed — skip bad files; validate SS58
#160053 MultisigCreated mis-attribution Fixed — correlate creator/signers/threshold/nonce
#160582 Raw --password CLI credentials Fixed — reject at helper boundary
#160592 Keystore permissive permissions Fixed — dir 0700, files 0600
#160593 Unauthenticated address redirect Fixed — integrity check on decrypt
#160594 Unauthenticated metadata substitution Fixed — passwordless paths stop trusting envelope
#160598 Filesystem races in wallet storage Fixed — locks, random temps, name checks
#160605 Failed legacy migration bypass Fixed — fail closed on migration save
#160611 Watched txs succeed when absent Fixed — missing extrinsic → error
#160612 Unsafe retries duplicate txs Fixed — single submit, no nonce-bump retry
#160624 Unverified RPC signing context Fixed — Quantus runtime identity gate
#160655 Batch not atomic Fixed — utility.batch_all
#160708 Password-file permission checks Fixed — Unix owner-only required
#160716 Legacy AES key alongside ciphertext Already fixed (e0be480); residual: refuse re-persisting digests
#160737 Wallet creation not atomic Fixed — exclusive create / hard_link
#160748 Ephemeral mnemonic strands funds Fixed — require persisted mnemonic
#160754 Transfer events unbound Fixed — match from/amount/count
#160773 Self-update without integrity check Fixed — verify published SHA-256
#160791 Unvalidated RPC token properties Fixed — fail-closed decimals/symbol/ss58

Medium (30/30 addressed)

ID Title Outcome
#159340 Version/nonce panics on decrypt Fixed
#159469 Exported mnemonic on stdout Fixed — require --output (0o600)
#159662 Storage pagination loop/overflow Fixed
#159890 Spent transfers reported available Fixed
#159916 Single-block over-limit abort Fixed — offset pagination
#160052 Duplicate signers Fixed — sort+dedup
#160103 Wormhole --secret argv Fixed — --secret-file
#160105 Secrets not zeroized after proof Fixed
#160110 Unbounded Merkle depth Fixed
#160591 Wallet secrets retained Fixed — Drop/zeroize/redacted Debug
#160595 Wallet name path escape Fixed (with #160598 name validation)
#160625 Unbounded tx-status waits Fixed — deadlines
#160640 Malformed pubkey panic Fixed — InvalidPublicKey
#160652 Token metadata / decimal format Fixed — checked_pow + validation
#160656 Transfer data / chain decimals Fixed
#160660 Removal missing member rank Fixed — required --min-rank
#160667 Recursive wormhole unfinalized Fixed — finalized snapshots
#160674 Delay conversion overflow Fixed — checked helpers
#160697 Circuit artifacts unauthenticated Fixed — manifest.json SHA-256
#160699 Artifact symlink redirection Fixed — refuse symlinks
#160700 Build artifact publish races Fixed — atomic publish
#160715 Exhausting Argon2 params Fixed — lock to generated profile
#160718 Preimage AlreadyNoted substring Fixed — verify on-chain
#160724 WS URL credentials in diagnostics Fixed — redact userinfo
#160732 Transfer total wrap Fixed — checked_add
#160734 Batch vs call-count limit Fixed — runtime batched_calls_limit
#160749 Failed extrinsic reported verified Fixed — failure-dominant
#160776 Missing aggregate bypasses split Fixed
#160777 Offset not global across ranges Fixed
#160783 Public helpers panic on bad input Fixed — fallible APIs

Low (11 Unreviewed — in scope)

ID Title Status in this PR
#159905 Byte-indexed address truncation on remote IDs Open
#159911 CLI transfer limit accepts values above documented 1000 Open
#159917 Fragile substring matching for limit-exceeded errors Open
#160136 Distribution invariant broken by u128 overflow Open (related hardening via checked adds elsewhere)
#160585 Password-file permits symlink targets / unbounded reads Partial — mode/owner checks added (#160708); symlink/size bounds still open
#160678 Malformed RPC header fields can panic CLI Open
#160711 Malformed wallet nonce panics during unlock Open
#160744 Unchecked RPC string slicing can crash system inspection Open
#160760 Unavailable home directories can panic Open
#160789 WalletManager lacks sync for concurrent FS ops Partial — keystore process lock / create locks from High #160598/#160737
#160800 Proposal IDs decoded from key suffix without layout validation Open

Excluded: 314 Low with Validity: Invalid

Out of scope per V12. No further triage required for merge of this PR.

Opportunistic hardening (Invalid Lows — not audit blockers)

Some Invalid Lows were still tightened while adjacent to High/Medium work (e.g. block-list bounds, storage iterate cap, JSON numeric parsing, multisend dupes, metadata checked_add). These are optional defense-in-depth, not required to close the Unreviewed set.

Info (2 Unreviewed)

  • #160685 Bind deposits/votes to confirmed referendum index — informational
  • #160730 Non-native leaves represented as native assets — informational

Breaking / UX changes callers should know

  • wallet import / from-seed: no --mnemonic / --seed flags (stdin prompts)
  • --password / -p rejected everywhere; use --password-file, env, or prompt
  • wallet create no longer silently uses an empty password; prompts (with confirm), --password-file, or env; empty only via --allow-empty-password
  • Wormhole: --secret--secret-file
  • wallet export: requires --output file (no stdout mnemonic dump)
  • Tech collective remove: requires --min-rank
  • Circuit artifacts: need a rebuild so generated-bins/ is a real directory with manifest.json (symlink-style bins rejected)
  • QuantusClient::new rejects non-Quantus / incompatible runtimes
  • Batch transfers use batch_all (atomic fail-all)

Test plan

  • cargo test --lib (211 passed)
  • Manual: quantus wallet import --name x --mnemonic '...' fails clap parse
  • Manual: quantus wallet create --name x --password secret errors with guidance
  • Manual: wallet dir/files are 0700/0600 after create
  • Manual: connect to wrong specName RPC fails
  • Manual: quantus update refuses checksum mismatch (if exercising updater)
  • Full circuit rebuild without SKIP_CIRCUIT_BUILD once for new generated-bins layout
  • Smoke send / multisig create / wormhole prove against local node

Commits

3d781aa fix(wallet): require an explicit password when creating wallets
ccc244a fix(cli): bound ranges and reject silent zero coercions
7f569f9 fix(bins): authenticate circuit artifacts and publish atomically
fdaee0c fix(cli): validate amounts delays ranks and fallible address helpers
06997c2 fix(wallet): zeroize secret material after encrypt and decrypt
8cd2d4c fix(wormhole): validate Merkle depth and prefer finalized snapshots
fbd38ed fix(wormhole): zeroize proof-generation secrets after use
fb5b36f fix(subsquid): harden exhaustive transfer queries and spent filtering
bcdab11 fix(batch): enforce runtime batched_calls_limit for batch size
12d3ff6 fix(rewards): use checked addition for indexer transfer totals
5015a2e fix(tx): bound transaction-status subscription waits
b7039e6 fix(storage): bound pagination against overflow and stuck cursors
1e9e910 fix(multisig): deduplicate signers before predict and threshold
0bec0ea fix(wallet): write exported mnemonics to a protected file
a710bf6 fix(wormhole): remove --secret argv and verify extrinsic failures
986c2cd fix(client): redact WebSocket URL credentials in diagnostics
95c1fc9 fix(wallet): return errors for malformed public keys
eeaefa7 fix(wallet): refuse to persist wallets with embedded AES key material
c5d3f2d fix(update): verify release archive SHA-256 before install
5fc7920 fix(wormhole): bind transfer events to from amount and count
304d6d5 fix(system): fail closed on invalid RPC token properties
a8051b1 fix(client): verify Quantus runtime identity at connect time
bfd228e fix(wallet): harden storage races and exclusive wallet creation
02c741a fix(wallet): authenticate address metadata and fail closed on migration
dfd96c0 fix(tx): stop unsafe nonce-bump retries on ambiguous errors
f111d9b fix(wormhole): require persisted mnemonic for HD secrets
0460883 fix(batch): use utility.batch_all for atomic transfers
27d8a9f fix(wallet): require restrictive password-file permissions
3bc368c fix(wallet): enforce owner-only keystore permissions
58b8780 fix(tx): fail when watched extrinsic is missing from block
e6dd668 fix(wallet): reject raw --password CLI credentials
d2ac243 fix(multisig): correlate MultisigCreated to creator and params
253c791 fix(wallet): skip malformed files when listing wallets
03dad4d fix(wallet): stop accepting mnemonic and seed via CLI flags

illuzen and others added 30 commits August 4, 2026 14:30
Authority secrets on --mnemonic/--seed were visible in process argv,
shell history, and CI logs. Always read them from a hidden prompt instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
A single corrupt or non-canonical wallet JSON aborted the entire list.
Validate Quantus SS58 addresses at load and isolate per-file failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
Taking the first same-block MultisigCreated event could report another
transaction's address. Match creator, signers, threshold, and nonce.

Co-authored-by: Cursor <cursoragent@cursor.com>
Passwords on --password/-p were visible in process argv and logs.
Reject them at the shared helper and wallet-create boundary.

Co-authored-by: Cursor <cursoragent@cursor.com>
check_execution_success treated a missing extrinsic hash as success.
Return a NetworkError so callers do not report false confirmations.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wallet directories and files inherited umask defaults, allowing local
users to read ciphertext and KDF metadata. Set dir 0700 and files 0600.

Co-authored-by: Cursor <cursoragent@cursor.com>
--password-file accepted world-readable files. On Unix, require a
regular file owned by the caller with no group/other access bits.

Co-authored-by: Cursor <cursoragent@cursor.com>
User-facing batch transfers used non-atomic utility.batch while docs
promised fail-all semantics. Switch the builder to batch_all.

Co-authored-by: Cursor <cursoragent@cursor.com>
Generating an ephemeral mnemonic when a wallet had none could strand
funds at irrecoverable addresses. Error instead and require a mnemonic.

Co-authored-by: Cursor <cursoragent@cursor.com>
Error-substring retries re-signed with an incremented nonce and could
duplicate extrinsics. Submit once with a fresh nonce and surface Subxt errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
Envelope address was trusted without keypair binding, enabling transfer
redirect and spoofed listings. Validate on decrypt, stop passwordless
trust of the envelope, and propagate legacy migration save failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
Predictable temp paths and check-then-write overwrites allowed races and
wallet replacement. Use exclusive create, safer temps, name checks, and locks.

Co-authored-by: Cursor <cursoragent@cursor.com>
RPC connections trusted any node metadata for signing context. Require
spec name quantus and a compatible runtime version before proceeding.

Co-authored-by: Cursor <cursoragent@cursor.com>
Missing or out-of-range tokenDecimals/symbol/ss58Format silently
mis-scaled amounts. Validate properties before using them for formatting.

Co-authored-by: Cursor <cursoragent@cursor.com>
Destination-only matching could select another same-block transfer.
Require a unique match on from, amount, and transfer_count.

Co-authored-by: Cursor <cursoragent@cursor.com>
Self-update applied GitHub archives without checking published
sha256sums. Download the sibling checksum file and verify before replace.

Co-authored-by: Cursor <cursoragent@cursor.com>
Legacy Argon2 digests must not be rewritten to disk. Encrypt already
strips digests; save paths now reject any remaining embedded key material.

Co-authored-by: Cursor <cursoragent@cursor.com>
Address derivation previously panicked on bad Dilithium public key
bytes. Propagate InvalidPublicKey through wallet creation and views.

Co-authored-by: Cursor <cursoragent@cursor.com>
Node URLs with userinfo were logged and embedded in errors. Sanitize
diagnostics so passwords are not disclosed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Secrets on --secret were visible in process argv; require --secret-file.
Also treat ExtrinsicFailed as dominant so failed txs are not verified.

Co-authored-by: Cursor <cursoragent@cursor.com>
Printing mnemonics to stdout risked shoulder-surfing and log capture.
Require --output and write an owner-only file instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
Duplicate signers inflated predicted addresses and could satisfy
thresholds incorrectly. Sort and dedup before prediction and checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
Checked count accumulation and reject non-advancing key cursors so
malicious RPC pages cannot loop or wrap the entry count.

Co-authored-by: Cursor <cursoragent@cursor.com>
Unbounded tx_progress waits could hang forever. Apply inactivity and
overall deadlines and surface stream timeouts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Untrusted Subsquid amounts were summed with wrapping u128 +=.
Reject overflows instead of silently wrapping totals.

Co-authored-by: Cursor <cursoragent@cursor.com>
Batch sizing used a soft heuristic that could exceed the runtime call
count limit. Read Utility::batched_calls_limit and fail closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Require aggregate counts, offset-paginate single over-limit blocks, apply
caller offset once globally, and exclude spent nullifiers from pending sets.

Co-authored-by: Cursor <cursoragent@cursor.com>
Proof inputs retained secret bytes after generation. Clear them before
returning so secrets do not linger in process memory.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reject oversized/mismatched ZK Merkle proofs and read recursive flow
state from finalized blocks instead of best-block tips.

Co-authored-by: Cursor <cursoragent@cursor.com>
Key material and plaintext buffers were retained after use. Clear them
on Drop and after crypto operations; redact Debug output.

Co-authored-by: Cursor <cursoragent@cursor.com>
illuzen and others added 9 commits August 5, 2026 13:20
--mnemonic put a full seed phrase on argv, which is strictly more
sensitive than the --secret argv that was already removed. Mirror
--secret-file with --mnemonic-file and reject the old flag.

Co-authored-by: Cursor <cursoragent@cursor.com>
Defaulting to the best block lets reorgs invalidate proofs before
finality. Match recursive flows and use chain_getFinalizedHead when
--at-block is not set.

Co-authored-by: Cursor <cursoragent@cursor.com>
The verify_private/public_batch submitters waited on an unbounded
status stream. Reuse wait_tx_inclusion so they share the same
inactivity and overall-deadline limits as signed watches.

Co-authored-by: Cursor <cursoragent@cursor.com>
After the initial dissolve transfer, events were fetched from the
moving finalized tip, which can skip the inclusion block after funds
moved. Use the watched inclusion hash instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Submit errors now surface as QuantusError::Subxt without the old
"Failed to submit transaction" wrapper, so the fuzz classifier treated
valid pool rejections as unclean. Match SubXT validity strings, and
delete the constant-false is_retryable_submission_error tombstone and
unused get_incremented_nonce_with_client helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
build.rs and bins.rs each defined the manifest file list, so a one-sided
edit produced "file set mismatch" against freshly built artifacts. Keep
the single source of truth next to the other shared bins constants.

Co-authored-by: Cursor <cursoragent@cursor.com>
Alerts were almost entirely test/example noise; cargo audit remains the
dependency CVE gate. Document the decision and how to restore a narrower
Actions-hygiene check later if wanted.

Co-authored-by: Cursor <cursoragent@cursor.com>

@n13 n13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 44 files against the audit-finding claims, with the branch checked out locally: read the full diff, traced the load-bearing paths in context, and ran the suite (cargo test --lib: 226 pass; the one failure is the known SKIP_CIRCUIT_BUILD environment artifact, and CI is fully green).

Overall: this is a genuinely strong remediation. The overwhelming majority of the 50 High/Medium claims are implemented for real and well tested — wallet storage races (O_EXCL temps + hard_link create), envelope-address authentication with fail-closed migration, argv secret removal with clap-level tests, export to 0600 file, wormhole depth bounds / event binding / failure-dominant verification / finalized inclusion blocks, batch_all + runtime batched_calls_limit, storage pagination bounds, subsquid fail-closed aggregate + global offset (logic verified correct, incl. ordering), checked arithmetic, fail-closed token properties, and an atomic self-update flow with no partial-install path. One issue is blocking; the rest are majors/minors that are either small in-PR fixes or reasonable fast-follows.

Blocking

1. New stale-artifact quarantine can silently delete a user-pointed directory tree — including wallets.
src/bins.rs:106-116remove_path_nofollow(&dir) (src/bins_fs.rs:10-48) renames aside and remove_dir_alls the entire directory that QUANTUS_BINS_DIR resolves to, not just the artifact files. Scenario: a user sets QUANTUS_BINS_DIR=$HOME/.quantus (wallets live at ~/.quantus/wallets, src/wallet/mod.rs:51-54); generate() writes artifacts + manifest.json straight into it; after the next CLI version bump stale_artifact_provenance returns Some and the next wormhole command deletes ~/.quantus wholesale. Pre-PR code never deleted the directory. Fix: quarantine only the manifested/REQUIRED_FILES entries, or refuse to quarantine when the directory contains unexpected entries.

Major — worth fixing in this PR

2. MultisigCreated correlation (#160053) reads events from the moving tip, not the inclusion block.
Both create paths (src/cli/multisig.rs:568-569, src/cli/multisig.rs:1201-1202) do get_latest_block() + events().at(...) after the watch returns. This PR itself added submit_transaction_with_inclusion_block (src/cli/common.rs:486) with a doc comment saying event readers must use the inclusion block, and migrated wormhole — but not multisig. With --finalized (up to 30 min wait) the tip is far past the inclusion block, so the strictly-correlated event is essentially never found and the command warns "Couldn't find MultisigCreated event" yet returns Ok. Not a security hole (strict matching prevents printing a wrong address), but the fix misses in exactly the racy case it was built for.

3. The 30s pre-inclusion inactivity timeout (#160625) is too tight for a 10s-target PoW chain and reintroduces the retry-duplication hazard #160612 removed.
TX_STATUS_INACTIVITY_TIMEOUT_SECS = 30 (src/cli/common.rs:14) is applied per status-stream event before inclusion, but the stream is silent between Broadcasted and InBestBlock — one block interval. With exponential PoW intervals, P(gap > 30s) ≈ e⁻³ ≈ 5%: roughly 1 in 20 watched txs aborts with a timeout while the tx remains valid in the pool and executes later; a script that retries on failure duplicates the transfer. The bounded-deadline design is right — the constant isn't. Suggest several block intervals (e.g. 120s), or reconcile against on-chain nonce/extrinsic presence before reporting failure.

Major — acceptable as fast-follows, but please track

4. Argon2 accepted profile is pinned to the crate's defaults — a future argon2 bump silently bricks every existing wallet.
src/wallet/keystore.rs:658-660 requires decrypt-side params to equal Params::DEFAULT_M_COST/T_COST/P_COST while encrypt uses Argon2::default(). These defaults are crate properties and already changed between argon2 0.4→0.5; after a future bump, every wallet on disk fails with a bare Decryption error while roundtrip tests stay green (new wallets are self-consistent). One-line fix: freeze the profile as literals (19456, 2, 1).

5. Zeroization (#160105/#160591) is real at the library boundary but nullified by surviving caller-side copies.
src/wormhole_lib.rs:228-241 wipes input.secret, but the same 32 bytes persist un-wiped in callers: the Copy local at src/cli/wormhole.rs:2824/2891, hex Strings of the secret at src/cli/wormhole.rs:2412 and :3743, and the whole Vec<DissolveOutput> of raw secrets (src/cli/wormhole.rs:3471-3473, populated :3643, :3790). Similarly on the wallet side: mnemonic/seed copies at src/wallet/mod.rs:107, :262-263, :420-427, src/cli/wallet.rs:648, :710-712, and src/wallet/password.rs:61-63 drop un-zeroized. What was done (Drop impls, redacted Debug, decrypt-buffer wipe) is real, but the audit items should not be marked fully closed.

6. Circuit-artifact "authentication" (#160697) doesn't stop the realistic attacker.
The manifest is unsigned and lives in the directory it authenticates, and resolve_bins_dir prefers ./generated-bins from the current working directory (src/bins.rs:75-78). Running any wormhole command inside an untrusted checkout loads that checkout's bins; the attacker ships matching manifest hashes and verify_manifest passes. Verification-on-load itself is correctly enforced with no missing-manifest bypass (src/bins.rs:117-122, src/batch_verifier.rs:130) — but consider refusing or warning on the CWD source.

7. A security-remediation PR removes the repo's only SAST.
.github/workflows/codeql.yml is deleted (commit ea9318e, rationale in PR_V12_SECURITY.md). The stated alternative (paths-ignore / test filters) is conceded in the rationale itself, and cargo audit (known dependency CVEs) plus clippy don't replace source-level taint analysis or the Actions-hygiene rules. Recommend landing a narrowed workflow instead of deleting outright.

8. Self-update checksum (#160773) is same-origin integrity, not authenticity.
The expected SHA-256 is a sibling asset of the same GitHub release fetched over the same channel (src/cli/update.rs:236-267). It stops corruption and single-object substitution; it does not stop anyone who can write release assets or MITM TLS (they control both files). The implementation itself is correct and atomic (verified against self_update 0.43 internals). self_update has a zipsign/ed25519 signatures feature worth adopting; at minimum the claim shouldn't be read as signing.

Minor / notes

  • src/chain/client.rs:145 + src/config/mod.rs:31-43 — the runtime identity gate (#160624) checks only self-reported specName/versions (no genesis-hash pin): it's an anti-footgun, not malicious-RPC protection. It also now runs on every QuantusClient::new, including read-only commands — each runtime upgrade bricks the CLI until COMPATIBLE_RUNTIMES is extended. Worth a release note.
  • src/subsquid/client.rs:313-351 — the single-block offset paginator assumes every non-final page has exactly 1000 rows; a Hasura deployment with a lower API row cap silently drops rows. Require page.len() == SERVER_MAX_LIMIT for non-final pages (and check transfers.len() == total_count in the small-block branch).
  • src/wallet/keystore.rs:71-80 — read-side O_NOFOLLOW is Linux/Android-only (the hardcoded constant is wrong for macOS, so macOS gets a no-op). libc::O_NOFOLLOW would make it portable.
  • src/wallet/keystore.rs:250-261 — infallible to_account_id_32 falls back to the all-zero account on malformed keys. Currently unreachable (decrypt integrity check guarantees validity), but it converts the fixed panic into a silent wrong answer, and src/cli/wormhole.rs:2123 builds a funding target from it. Prefer removing the infallible variants.
  • -p/--password is still an advertised clap flag everywhere with help text inviting use; rejection is runtime-only (the helper boundary is complete — verified all keypair paths). Suggest hide = true. Also wallet import prompts for the mnemonic before rejecting --password (src/cli/wallet.rs:648 vs :651).
  • src/cli/common.rs:904 — preimage verification (#160718) reads get_latest_block() instead of the inclusion block; same pattern (and same fix) as finding 2.
  • Behavior regressions not in the PR description: resolving a destination by wallet name now requires that wallet's password (src/cli/common.rs:336-384), and transfers query (non-JSON) now hard-requires a live node and aborts on one malformed indexer row (src/cli/transfers.rs:193-204). Both defensible; both should be in the release notes.
  • src/cli/update.rs:262 — the archive is fully downloaded before any size check or verification (resource exhaustion by rogue asset); :194/:213 — a non-semver latest tag reports "already latest" via unwrap_or(false) instead of erroring.
  • src/bins.rs:412 — tests import std::os::unix::fs::symlink without #[cfg(unix)]; cargo test won't compile on Windows, which the release pipeline ships for.
  • PR_V12_SECURITY.md is a copy of the PR description committed to the repo root and references a v12-issues.md that isn't in the repo — it will go stale immediately; suggest dropping it from the branch (or moving to docs/ with the referenced file).

Verdict: Request changes. Blocking: the quarantine deletion path (finding 1). Findings 2 and 3 are small, well-localized fixes I'd also want before merge since they sit at the heart of two claimed High fixes. Everything else is fast-follow material — the security substance of this PR is real and the test discipline (red→green, tamper/symlink/permutation cases) is genuinely good.

illuzen and others added 17 commits August 5, 2026 15:37
remove_path_nofollow(&dir) deleted the whole resolved bins directory,
so QUANTUS_BINS_DIR=~/.quantus would wipe wallets on the first upgrade.
Remove only the bounded set of known artifact filenames (including the
legacy prover.bin), refuse directories occupying those names, and leave
every other entry untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>
Both create paths read events from the moving tip after the watch
returned, so with --finalized the strictly-matched event was almost
never found. Use submit_transaction_with_inclusion_block like the
wormhole flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
The status stream is silent for a full PoW block interval between
Broadcasted and InBestBlock; with ~exponential 10s intervals a 30s
window aborted ~1 in 20 valid transactions, inviting duplicate-submit
retries. Use twelve target intervals and warn in both timeout errors
that the transaction may still execute.

Co-authored-by: Cursor <cursoragent@cursor.com>
Encrypt used Argon2::default() and decrypt pinned to Params::DEFAULT_*,
which are crate properties that already changed between argon2 0.4 and
0.5 - a future bump would silently brick every wallet on disk while
self-consistent roundtrip tests stayed green. Freeze m=19456/t=2/p=1 as
literals shared by both sides and pin them with a test.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ation honestly

Library-boundary zeroization was nullified by surviving caller copies. Wipe
the ones the current type shapes allow: hex-encoded secrets in multiround/
dissolve proof loops, the parsed secret local in generate_proof,
DissolveOutput secrets (zeroize-on-drop + redacted Debug), prompted
mnemonic/seed strings in wallet import/from-seed, seed copies in
WalletManager create paths, and raw password-file reads. Downgrade #160105/
#160591 to partially fixed in the PR doc with a scope note: Copy arrays and
String reallocation make full closure impossible without non-Copy wrapper
types end to end.

Co-authored-by: Cursor <cursoragent@cursor.com>
The artifact manifest is unsigned and lives in the directory it
authenticates, so an attacker-prepared checkout could ship a
self-consistent ./generated-bins that passes verify_manifest, and
resolve_bins_dir would silently prefer it over the per-user store.
Refuse the implicit CWD source with an actionable error; local dev opts
in explicitly via QUANTUS_BINS_DIR=./generated-bins.

Co-authored-by: Cursor <cursoragent@cursor.com>
Deleting the workflow removed the repo's only first-party SAST and the
Actions-hygiene checks. Bring it back with the default high-precision
security suite (not security-and-quality) and examples/ path-excluded,
which addresses the noise that motivated the removal while keeping
source-level taint analysis.

Co-authored-by: Cursor <cursoragent@cursor.com>
…egrity

The expected SHA-256 is a sibling asset of the same release fetched over
the same channel, so it stops corruption and single-object substitution
but not an attacker controlling release assets or the TLS channel. State
the threat model in the module doc and PR notes, and point at
self_update's zipsign/ed25519 signatures feature as the authenticity
follow-up (needs release-pipeline signing first).

Co-authored-by: Cursor <cursoragent@cursor.com>
…g rows

The offset paginator assumed every non-final page holds exactly the
requested 1000 rows; a Hasura deployment with a lower API row cap would
return fewer and the missing rows would be silently skipped. Require
exact page sizes (full pages, remainder on the last) and verify the
small-range branch returns exactly total_count rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
The hardcoded 0o400000 constant is Linux's O_NOFOLLOW; on macOS the flag
is 0x0100, so wallet reads there silently followed symlinks. Use
libc::O_NOFOLLOW on all Unix targets and add a test that loading a
wallet through a symlink fails (verified on macOS).

Co-authored-by: Cursor <cursoragent@cursor.com>
to_account_id_32 fell back to the all-zero account and
to_account_id_ss58check to an empty string on malformed keys, turning a
detectable error into a silent wrong answer that callers (including the
wormhole funding-target path) could act on. Remove both and propagate
errors through the try_ variants at every call site.

Co-authored-by: Cursor <cursoragent@cursor.com>
…jection

-p/--password has been rejected at runtime since the password-policy
change, but every command still advertised it in --help with text
inviting use. Hide the flag everywhere (still parsed, so the runtime
rejection message keeps guiding existing scripts). Also resolve the
password policy before the mnemonic/seed prompt in wallet import and
from-seed, so a doomed invocation no longer collects the secret first.

Co-authored-by: Cursor <cursoragent@cursor.com>
verify_preimage_on_chain read storage at get_latest_block() after the
watch returned - the same moving-tip race fixed for multisig event
correlation. Use submit_transaction_with_inclusion_block and read the
preimage at the block the extrinsic landed in; the already-noted
fallback path keeps reading the tip since no inclusion block exists.

Co-authored-by: Cursor <cursoragent@cursor.com>
Assets were fully downloaded before any size check, letting a rogue
release asset exhaust disk/memory before verification; wrap downloads
in a limiting writer (64 KiB for sha256sums, 512 MiB for the archive).
Also stop reporting "already latest" when the latest tag fails semver
parsing - bump_is_greater errors now surface instead of unwrap_or(false).

Co-authored-by: Cursor <cursoragent@cursor.com>
The tests module imported std::os::unix::fs::symlink unconditionally,
so cargo test would not compile on Windows, which the release pipeline
ships for.

Co-authored-by: Cursor <cursoragent@cursor.com>
The file is a copy of the PR description and references v12-issues.md
which is not in the repo; it would go stale immediately after merge.
Keep it as an untracked working file (gitignored, along with
v12-issues.md) so it can still be pasted into the PR description.

Co-authored-by: Cursor <cursoragent@cursor.com>

@n13 n13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the 17 follow-up commits (3e7d260..5cfead6) with the same procedure as the first pass: full incremental diff read, every fix verified in context on a local checkout, suite re-run (cargo test --lib: 235 pass, up from 226 with the new tests; the single failure remains the known SKIP_CIRCUIT_BUILD environment artifact).

Every finding from the previous review is addressed, and I found no new bugs introduced by the fixes.

Verified fixed

  • Blocking — destructive quarantine: remove_stale_artifact_files (src/bins.rs:196-227) now deletes only a bounded set of exact artifact filenames via symlink_metadata + nofollow removal, refuses a directory occupying an artifact name, and never touches the containing directory. The wallets-adjacent scenario is directly covered by stale_quarantine_preserves_unrelated_entries.
  • Multisig correlation: both create paths use submit_transaction_with_inclusion_block and read events at the returned inclusion hash (src/cli/multisig.rs:559-576, :1191-1214); the watch machinery genuinely returns the block containing the extrinsic in both best and finalized modes, and the no-wait path is safe.
  • Inactivity timeout: now 120s with const-asserted ordering against the 300s/1800s deadlines (src/cli/common.rs:20, :1154-1162); disambiguation boundary conditions check out; error text now warns against blind resubmission.
  • Argon2 profile: frozen as literals 19_456/2/1 used by both encrypt and decrypt (src/wallet/keystore.rs:61-72); legacy-wallet decrypt test still passes and a new test pins the written PHC string. The bricking trap is closed.
  • Zeroization: the named caller-side copies are wiped for real (write_volatile + fence) — the Copy local and hex Strings in wormhole, DissolveOutput now zeroize-on-Drop with redacted Debug, prompted mnemonic/seed and password-file reads wiped.
  • CWD circuit artifacts: untrusted ./generated-bins is now a hard error with an actionable opt-in via QUANTUS_BINS_DIR (src/bins.rs:80-98); CI/exercise flows verified unaffected.
  • Self-update: 64 KiB / 512 MiB caps enforced during streaming (src/cli/update.rs:356-387), non-semver tags now error in both check and install paths, and the module doc honestly scopes the checksum to same-origin integrity with the zipsign path named as future work.
  • CodeQL reinstated as a narrowed workflow (rust + actions, high-precision suite, paths-ignore: examples), and the smaller items (portable O_NOFOLLOW — symlink-refusal test now passes on macOS where the old constant was a no-op; infallible to_account_id_* removed with all call sites migrated to ? propagation; --password hidden everywhere with reject-before-prompt; preimage verified at inclusion block; subsquid short-page rejection with no off-by-one on the final page; cfg(unix) test gating; PR_V12_SECURITY.md dropped) all check out.

One thing to resolve before merge

The new CodeQL check is red on this PR — but the alerts are false positives. I sampled the 26 "critical" + 12 "high" alerts: the criticals are rust/hard-coded-cryptographic-value firing on the deliberate empty-password probe (decrypt_wallet_data(&encrypted_wallet, "") — how passwordless wallets are detected), test passwords, and even a 0o700 permission constant; the highs are rust/cleartext-logging on test println!s of public addresses, the generic src/log.rs macros, and example code. Nothing real in the samples. The check still needs to go green: dismiss the alerts as false positives, and consider paths-ignore for #[cfg(test)]-heavy noise or excluding the generic log macros — your own workflow is doing its job loudly on its first run over this branch.

Low-severity residuals (fine as follow-ups)

  • src/bins.rs:196-227 — quarantine deletes .quantus-cli-version/manifest.json early in the BTreeSet iteration; an abort partway (e.g. the dir-refusal path) strands artifacts with provenance gone, and the subsequent hard-error message says "remove the directory and rerun" — which for QUANTUS_BINS_DIR=~/.quantus advises deleting the directory containing wallets. Remove the manifest/marker last and name the specific files in the message.
  • Zeroization stragglers: src/wallet/mod.rs:107 (mnemonic.clone() — the original still drops un-wiped; trivial move fixes it), src/wallet/mod.rs:314-317 (seed64 in import_wallet_no_derivation, inconsistent with the fixed sibling at :262-266), src/cli/wallet.rs:612-620 (exported mnemonic String), and early-error paths in wormhole's generate_proof before the wipe point.
  • src/cli/multisig.rs:1485-1486fetch_proposal_id still correlates ProposalCreated at get_latest_block(); same pattern as the fixed MultisigCreated issue, but degrades gracefully to the "run list-proposals" hint rather than mis-reporting.
  • Dev-ergonomics note: after a full local build, wormhole commands from the repo root now hard-error until QUANTUS_BINS_DIR=./generated-bins is exported; the message is actionable but README doesn't document the opt-in (its bins section also still describes a developer build-circuits command that no longer exists — pre-existing staleness).
  • Subsquid short-page rejection branches have no dedicated unit tests (existing pagination tests only pass through the new checks).

Verdict: Approve. All blocking and major findings are properly fixed with test coverage, and no new issues were introduced. Please get the CodeQL check green (false-positive dismissals and/or workflow scoping) before merging; the residuals above are non-blocking follow-up material.

Even narrowed to the default security suite with examples/ excluded,
CodeQL's Rust analysis produces noise without actionable signal on this
crate. Drop the workflow; cargo audit and clippy remain in ci.yml, and
the actions-hygiene rules can come back later as a narrow actions-only
workflow if wanted.

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen
illuzen merged commit 15393c5 into main Aug 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants