V12 issues - #126
Conversation
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>
--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
left a comment
There was a problem hiding this comment.
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-116 → remove_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-reportedspecName/versions (no genesis-hash pin): it's an anti-footgun, not malicious-RPC protection. It also now runs on everyQuantusClient::new, including read-only commands — each runtime upgrade bricks the CLI untilCOMPATIBLE_RUNTIMESis 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. Requirepage.len() == SERVER_MAX_LIMITfor non-final pages (and checktransfers.len() == total_countin the small-block branch).src/wallet/keystore.rs:71-80— read-sideO_NOFOLLOWis Linux/Android-only (the hardcoded constant is wrong for macOS, so macOS gets a no-op).libc::O_NOFOLLOWwould make it portable.src/wallet/keystore.rs:250-261— infallibleto_account_id_32falls 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, andsrc/cli/wormhole.rs:2123builds a funding target from it. Prefer removing the infallible variants.-p/--passwordis still an advertised clap flag everywhere with help text inviting use; rejection is runtime-only (the helper boundary is complete — verified all keypair paths). Suggesthide = true. Alsowallet importprompts for the mnemonic before rejecting--password(src/cli/wallet.rs:648vs:651).src/cli/common.rs:904— preimage verification (#160718) readsget_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), andtransfers 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" viaunwrap_or(false)instead of erroring.src/bins.rs:412— tests importstd::os::unix::fs::symlinkwithout#[cfg(unix)];cargo testwon't compile on Windows, which the release pipeline ships for.PR_V12_SECURITY.mdis a copy of the PR description committed to the repo root and references av12-issues.mdthat isn't in the repo — it will go stale immediately; suggest dropping it from the branch (or moving todocs/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.
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
left a comment
There was a problem hiding this comment.
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 viasymlink_metadata+ nofollow removal, refuses a directory occupying an artifact name, and never touches the containing directory. The wallets-adjacent scenario is directly covered bystale_quarantine_preserves_unrelated_entries. - Multisig correlation: both create paths use
submit_transaction_with_inclusion_blockand 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/1used 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) — theCopylocal and hexStrings in wormhole,DissolveOutputnow zeroize-on-Drop with redacted Debug, prompted mnemonic/seed and password-file reads wiped. - CWD circuit artifacts: untrusted
./generated-binsis now a hard error with an actionable opt-in viaQUANTUS_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 (portableO_NOFOLLOW— symlink-refusal test now passes on macOS where the old constant was a no-op; infallibleto_account_id_*removed with all call sites migrated to?propagation;--passwordhidden 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.mddropped) 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.jsonearly in theBTreeSetiteration; 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 forQUANTUS_BINS_DIR=~/.quantusadvises 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(seed64inimport_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'sgenerate_proofbefore the wipe point. src/cli/multisig.rs:1485-1486—fetch_proposal_idstill correlatesProposalCreatedatget_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-binsis exported; the message is actionable but README doesn't document the opt-in (its bins section also still describes adeveloper build-circuitscommand 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>
Summary
Addresses the V12 security audit findings in
v12-issues.md.Scope: only findings with
Validity: Unreviewed. V12 marks 314 Low findings asValidity: Invalid(likely incorrect); those are excluded from this analysis and are not treated as open work.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)
--passwordCLI credentials0700, files0600utility.batch_alle0be480); residual: refuse re-persisting digestsMedium (30/30 addressed)
--output(0o600)--secretargv--secret-fileInvalidPublicKeychecked_pow+ validation--min-rankmanifest.jsonSHA-256checked_addbatched_calls_limitLow (11 Unreviewed — in scope)
Excluded: 314 Low with
Validity: InvalidOut 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)
Breaking / UX changes callers should know
wallet import/from-seed: no--mnemonic/--seedflags (stdin prompts)--password/-prejected everywhere; use--password-file, env, or promptwallet createno longer silently uses an empty password; prompts (with confirm),--password-file, or env; empty only via--allow-empty-password--secret→--secret-filewallet export: requires--outputfile (no stdout mnemonic dump)--min-rankgenerated-bins/is a real directory withmanifest.json(symlink-style bins rejected)QuantusClient::newrejects non-Quantus / incompatible runtimesbatch_all(atomic fail-all)Test plan
cargo test --lib(211 passed)quantus wallet import --name x --mnemonic '...'fails clap parsequantus wallet create --name x --password secreterrors with guidance0700/0600after createspecNameRPC failsquantus updaterefuses checksum mismatch (if exercising updater)SKIP_CIRCUIT_BUILDonce for newgenerated-binslayoutCommits