sec: close the cargo-audit backlog — move the WASM sandbox off a runtime with a filesystem escape, an aarch64 miscompile and a fuel-limit bypass #443
Loading…
Reference in a new issue
No description provided.
Delete branch "sec/advisory-sweep-2026-10"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
cargo auditover all 47 workspace lockfiles reported 142 vulnerability instances. This closes the reachable ones and records, with evidence, why each survivor survives.The one that mattered
The S2 (WASM) executor was pinned to wasmtime 43.0.2, carrying eight advisories:
br_tableThree of these land squarely on the confinement claim rather than beside it:
-0269is a sandbox escape in the component whose only job is confinement,-0316defeats the fuel metering the S2 tier attests, and-0379is an aarch64 miscompile — the entire fleet is Apple Silicon, so that is the fleet's actual codegen path, not a remote arch.Moved to 49.0.4, the single version every one of the eight names as patched. 50.0.0 exists but no advisory lists it as fixed, so it is not yet the safe choice.
WASMTIME_VERSIONmoves with it: the constant is attested into the mechanism commitment under SPEC §7.9, so a stale value would attest confinement to a runtime that did not run. The crate's ownwasmtime_version_matches_the_lockfiletest enforces the pairing.Verified: builds clean and all 17 tests pass, including every confinement test (fuel exhaustion, egress allowlist, storage globs, taint propagation) — no API changes were needed across the six majors.
Reachable DoS on a payment path
quick-xmlRUSTSEC-2026-0194 (quadratic run time checking a start tag for duplicate attributes) is reachable:openpay/crates/op-iso20022/src/codec.rscallsquick_xml::de::from_stron ISO 20022 messages, which arrive from payment counterparties. Attacker-shaped XML is the normal case there. Bumped 0.36 → 0.41.0 in the four lockfiles where the version is ours to choose.-0195(unbounded namespace allocation inNsReader) is not reachable —NsReaderappears nowhere in the tree.The advisory with no fix
rsaRUSTSEC-2023-0071 (Marvin timing sidechannel) has no fixed version. It needed neither mitigation nor risk acceptance: the dependency was unused.rsa::,use rsaandextern crate rsaappear in none of op-3ds2's 20 source files nor anywhere in openpay. The declaration described an intent never implemented; only P-256 ECDH/ECDSA is actually present. Dropped, removingrsaandpkcs1from the graph.Semver-compatible bumps, no API change
rustls 0.23.46 · rustls-webpki 0.103.16 · webpki-roots & webpki-root-certs 1.0.10 · quinn-proto 0.11.18 (
-0340/-0343, both 7.5) · tokio 1.54.1 (-0384,Intervalrace) · crossbeam-epoch 0.9.20 · rtrb 0.3.5Not fixed, deliberately, with reasons
zip(9.1-0369, 7.3-0367,-0368) — not reachable. All three areextract()-only (SUID/SGID preservation, symlink escape, permissions through symlinks). We never extract an archive: there are noZipArchive/ZipStreamReadercall sites. It arrives transitively viacandle-core.hpke-rs0.2.0 (9.3-0071, 8.2-0070,-0069) — never shipped. The production MLS path (joule-moq-mls) already resolves the patched 0.7.0. The vulnerable 0.2.0 is reached only through a[dev-dependencies]edge ofjoule-code-collab, which still pins the older openmls 0.6/0.3 stack. Worth aligning to the 0.9.1/0.6.0 stack already used in-repo, but it is test-graph only.libcrux-kem0.0.9 (8.7-0331, 5.6-0330) — blocked upstream.hpke-rs-libcrux =0.7.0constrains it, so 0.0.10 needs an openmls/hpke-rs release. Both are panics (DoS), not memory corruption.quick-xml0.37.5 in openpay — blocked upstream, and this one is NOT closed. It resolves throughopen-payments-iso20022 v1.0.10, the same ISO 20022 surface. This PR hardens our own deserializer but not that library's internal parsing.quick-xml0.39.4 in smart-byte — lower exposure. Via plist → netdev → netwatch → iroh; parses macOS plists, not counterparty XML.Separately checked: the current PHP and Rust headlines
php_fastcgi/fastcgidirective in the Caddyfile (canonical and live agree, 3492 lines); 0.phpfiles across all 313 deployed site directories. The sites are static Astro builds behindfile_server.proc-macro1was never fetched into any cargo registry cache on dev or the build host;arrayrefresolves to 0.3.9 everywhere (only 0.3.10 was compromised);internmentto 0.8.6 (only 0.8.7). Zero compromised pins across all 13,241 lockfiles in the tree.🤖 Generated with Claude Code
Update — follow-up work on this branch
The quick-xml bump turned out to hide a silent data-corruption bug, which is the most important thing found after the first pass. 0.41 does not merely rename
BytesText::unescape: it reports every entity reference (&,',&) as its ownEvent::GeneralRefinstead of folding it into the surrounding text. Both XML readers here matched onlyEvent::Text, so after the bump each reference was dropped silently — the document still parsed, every existing test still passed, and the values were wrong:op-screeningparses the OFAC/EU/UN sanctions lists. An entry published as "Smith & Co" was stored as "Smith Co" — a name that no longer matches the party it names. A screening miss reads exactly like a clean screen.jouleclaw-transform-trxcarries JSON through TRX XML; dropping the references turned{"k":1}into{k:1}, which fails to parse and silently falls back to a bare string.Both readers now accumulate an element's character data across events and resolve each reference (numeric via
resolve_char_ref, named viaresolve_predefined_entity). TRX refuses an unresolvable name; the sanctions readers keep it in literal&name;form so the anomaly stays visible in the record.trim_text(true)had to go on all four readers too — trimming is per text event, so once a run is split the trim eats the spaces around the reference ("Smith & Co" → "Smith&Co"). The assembled value is trimmed once where it is consumed.The fixtures contained no entity anywhere, which is exactly why a silent corruption bug could pass CI. Added, failing first: a sanctioned name with
&and one with&(a named predefine and a numeric reference take different code paths), TRX entity references asserted against hand-written XML rather than our own writer's escaping, and a refusal for an unresolvable name.CI now gates on cargo-audit. The 142 instances this branch started from were not a decision — they accumulated because nothing was looking.
scripts/check-advisories.pyrunscargo audit --jsonover every tracked lockfile and fails on any advisory not named in its ALLOW table with a reason. Two rules keep that honest: an unlisted advisory fails, and a listed advisory that no longer appears also fails, so an excuse cannot outlive the exposure. Warnings (67, maintenance advice) are counted and printed but not gated. A lockfile that cannot be audited fails under its own heading — an error is never read as "clean", which is the exact mistake this sweep made by hand when a broken output parser reported "0 vulnerabilities" while criticals were printing. The self-test proves all four behaviours.hpke-rs 0.2.0 is gone. Rather than leave it excused as dev-only, the
joule-code-collabdev-deps were pinned to the generationjoule-moq-mlsalready runs (openmls 0.9.1 / 0.6.0). That removes hpke-rs 0.2.0, hpke-rs-crypto, hpke-rs-rust-crypto and openmls 0.6.0 outright, and with them RUSTSEC-2026-0069/-0070/-0071 (9.3)/-0072. One API change: 0.9 takes the crypto backend, not the provider, inexport_secret.tests/mls_keying.rspasses.Two corrections to what this PR said earlier
open-payments-iso20022 1.0.10declares quick-xml but never calls it — it ships serde-annotated ISO 20022 types (3 files, ~2.2k lines, noquick_xmlreference anywhere), and the parsing is ours inop-iso20022's codec, which binds the workspace 0.41. The old copy is compiled and never executed.wai-quantum-desktopfails to compile on unmodifiedorigin/main, not from these bumps:tauri::generate_context!()validatesfrontendDistat compile time and../quantum-studio/distdoes not exist. Confirmed identical at72a8ddda5. It is the only one of 23 workspaces that fails to check, which means CI does not build that binary — a separate gap, untouched here.Advisory count: 142 → 36 → 32 instances, 14 allowed and documented.
RUSTSEC-2023-0071 (Marvin attack: key recovery through timing sidechannels in RSA decryption) is the only finding in this sweep with **no fixed version available** — there is nothing to upgrade to. It needed neither a mitigation nor a risk acceptance, because the dependency was never used. `rsa::`, `use rsa` and `extern crate rsa` appear in none of op-3ds2's 20 source files, nor anywhere else in the openpay workspace. The declaration described an intent ("RSA for legacy issuer ACS public keys") that was never implemented; the only asymmetric crypto actually present is P-256 ECDH/ECDSA. Removing the line drops rsa 0.9.10 and pkcs1 0.7.5 from the graph. The adjacent comment still advertised RSA support, which would now mislead a reader into thinking it was dropped rather than never built. It is rewritten to say so, and to record that the crate cannot simply be re-added: the Marvin defect is unfixed upstream, so a future legacy-ACS path needs a constant-time implementation, not a version bump. Co-Authored-By: Claude Opus 5 <[email protected]>The quick-xml 0.41 bump did not just rename `BytesText::unescape`. 0.41 reports every entity reference — `&`, `'`, `&` — as its own `Event::GeneralRef` instead of folding it into the surrounding text. Both XML readers here matched only `Event::Text`, so after the bump each reference was dropped **silently**: the document still parsed, every existing test still passed, and the values were wrong. That is the dangerous shape of this change, and it is why it needed more than a compile fix: - `op-screening` parses the OFAC/EU/UN sanctions lists. An entry published as "Smith & Co" was being stored as "Smith Co" — a name that no longer matches the party it names, with nothing downstream able to tell. A screening miss reads exactly like a clean screen. - `jouleclaw-transform-trx` carries JSON property values through TRX XML. The quotes arrive escaped, so dropping the references turned `{"k":1}` into `{k:1}`, which fails to parse and falls back to being stored as a bare string. One round-trip test caught this; nothing else did. Both readers now accumulate an element's character data across events and resolve each reference: a numeric reference through `resolve_char_ref`, a named one through `resolve_predefined_entity`. TRX carries no DTD, so an unknown name there is refused rather than dropped. The sanctions readers keep an unknown reference in its literal `&name;` form, so the anomaly stays visible in the record instead of silently altering a name. `trim_text(true)` had to go with it, on all four readers. Trimming is applied per text event, so once a run is split by a reference the trim eats the spaces on either side of it — "Smith & Co" became "Smith&Co", which the new tests caught. The assembled value is trimmed once where it is consumed instead; the screening readers already did exactly that at every consumer, making the reader-level trim redundant there even before this change. Tests: the fixtures had no entity in them anywhere, which is precisely why a silent data-corruption bug could pass CI. Added, failing first: a sanctioned name carrying `&` and one carrying `&` (different code paths — a named predefine and a numeric reference), entity references in TRX text asserted against hand-written XML rather than our own writer's escaping, and a refusal for an unresolvable name. Co-Authored-By: Claude Opus 5 <[email protected]>The 142 vulnerability instances this branch started from were not the result of a decision. They accumulated because a vulnerable crate usually arrives transitively, in a lockfile nobody edited, and nothing in CI was looking. The fixes in this branch are worth little if the backlog can rebuild quietly. `scripts/check-advisories.py` runs `cargo audit --json` over every tracked Cargo.lock and fails on any advisory not named in its ALLOW table. It reads the advisory database and the lockfiles; it builds nothing. ALLOW is the substance. An advisory may be left in place — upstream has no fixed release, the vulnerable call is unreachable, it is dev-only — but only in writing, with the reason beside the id. Two rules keep that from rotting: - an unlisted advisory fails, so a new one gets looked at; - a listed advisory that no longer appears anywhere ALSO fails, so an excuse cannot outlive the exposure it was written for. An allow list that is never pruned stops describing the tree and starts hiding it. It currently allows 18, each with its reason: zip and rsa as unreachable, hpke-rs 0.2.0 as dev-only, and quick-xml/libcrux-kem/h2/hickory-proto/pyo3 as pinned by dependents that need an upstream release. The quick-xml 0.37.5 entry is marked as a known remaining exposure on the ISO 20022 ingestion path rather than a dismissal, so it reads as debt and not as a verdict. Warnings (unmaintained, unsound, yanked — 67 of them) are counted and printed but do not fail the run: gating on maintenance advice would bury the reachable defects in noise. Printing the count keeps the number visible instead of unknown. A lockfile that cannot be audited at all fails under its own heading, so an error is never read as "clean" — the same failure-closed shape as check-lockfiles.sh, and the mistake this sweep made by hand when a broken output parser reported "0 vulnerabilities" while criticals were printing. The self-test asserts the check passes a reviewed tree and fails each way it should: an unlisted advisory, an allowance matching nothing, and a document that does not parse. Co-Authored-By: Claude Opus 5 <[email protected]>