You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Main @ 0de7631b12b630b402faaac1df2079f63246566d (merge of #489). Prior audit @ 958e1ec (#474). Run via /general-audit.
Method
Per lessons #426 / #438 / #474 / #477: orchestrator-direct default for diffs under ~100 files / ~2000 LOC; pre-fetch existing-issue lists; orchestrator runs cargo-audit; sibling-of-closed pass on closed-since-last-audit.
0 agents launched. 66-file / +12733 −4961 LOC diff. Over the file-count threshold but under the finding-density threshold; orchestrator-direct still scaled because new code clusters in 3 areas (test-hooks foundation, voice/governance test landings, ProfileState/typing caps).
Pre-fetched existing-issue lists: /tmp/audit-issues.txt (17 entries), /tmp/security-issues.txt (26 entries), tech-debt list inline (17 entries). Total 60 unique titles for dedup.
Orchestrator ran cargo-audit directly. Clean vs CI ignore list (8 RUSTSEC IDs); 5 of 8 still match advisory DB, 3 ignore-list IDs no longer match anything (advisories may have been dropped upstream — non-finding).
Sibling-of-closed pass on PRs closed since 958e1ec. F1 + F2 are exactly siblings of ci:-titled commits whose actual scope was narrower than the bug class — 950e376 only edited just check-all, bd1f725 only added an eslint config file. CI workflows were never edited.
Direct main-context sweeps for: unsafe, dbg!/eprintln!/todo!/unimplemented!, Arc<Mutex>/Arc<RwLock> w/o lock-ok, panic!/unwrap in lib prod, js_sys::eval/innerHTML, TODO/FIXME/HACK, anyhow:: in pure libs, vector caps in wire types, let _ = h.<method>().await swallow (per general-audit lessons: 2026-04-28 #477 lesson), CSP, docker pin/USER hardening, SQL prep statements, voice cap enforcement, identity malformed-bytes tests.
SEC-V-05 follow-up (1a9503f): ProfileState.names and NetworkMeta.typing_peers LRU-capped at 10k, TTL sweep on typing_peers (5s) piggy-backed on existing 1Hz presence-tick driver.
lock-ok comments on every legitimate lib-crate Arc/Arc (12 sites: 4 client, 1 actor test, 1 web state_bridge cached, plus search/* doc-comment-only references)
unsafe sites all annotated (crates/actor/src/addr.rs:91 Sync for Addr, crates/web/src/state_bridge.rs:98 Send for DerivedStateActor)
no unimplemented!() / todo!() / dbg!() / eprintln!() / FIXME / HACK in lib production
docker images: digest-pinned rust:1.95-slim-bookworm, nginxinc/nginx-unprivileged:1.27-alpine, all containers run USER willow/USER nginx, trunk pinned 0.21.14
SQL queries in crates/storage/src/store.rs: parameterized (?N), values passed via Box<dyn ToSql> — no concatenation injection surface
test-hooks gated behind default = [], WillowTestHooks symbol absent from prod under spec — but enforcement gap, see F1
Findings (2 child issues)
#
Sev
Title
F1
medium
scripts/check-no-test-hooks-in-prod.sh symbol-leak guard not wired into CI / deploy workflows — only just check-all
F2
low
eslint.config.js bans waitForTimeout but no CI step or npm script runs eslint
Both findings = sibling-of-closed pattern. The ci:-prefixed commits (950e376, bd1f725) only edited local just recipe / config file but never touched .github/workflows/.
Findings deemed not actionable (verified during sweep)
RatchetCache::clear() has no production callers. PR fix(crypto): add RatchetCache::clear() for explicit key eviction (#178) #469 closed [security] RatchetCache retains old channel keys in memory without explicit eviction #178 by adding the API; production code path stores ChannelKey directly in crates/client/src/state.rs::ServerEntry.keys: HashMap<String, ChannelKey>, dropped via leave_server (crates/client/src/servers.rs:79) which triggers ZeroizeOnDrop per-entry. So there's no leak today, the API is precautionary library surface. Borderline tech-debt ("dead code in lib?") but RatchetCache is documented public crypto primitive — not dead, just not yet adopted. Leave as-is.
3 stale RUSTSEC ignore IDs in CI (RUSTSEC-2026-0098, 2026-0099, 2026-0104 no longer matched by current advisory DB scan). Cosmetic — won't break anything, but ignore list could be pruned. Not worth its own issue.
Diff was bigger (66 vs 52 files; +12733 vs +3525 LOC) — orchestrator-direct still scaled because finding density was low (most diff = test-hooks scaffolding + test landings, both well-gated and following spec)
Main @
0de7631b12b630b402faaac1df2079f63246566d(merge of #489). Prior audit @958e1ec(#474). Run via/general-audit.Method
Per lessons #426 / #438 / #474 / #477: orchestrator-direct default for diffs under ~100 files / ~2000 LOC; pre-fetch existing-issue lists; orchestrator runs cargo-audit; sibling-of-closed pass on closed-since-last-audit.
/tmp/audit-issues.txt(17 entries),/tmp/security-issues.txt(26 entries), tech-debt list inline (17 entries). Total 60 unique titles for dedup.958e1ec. F1 + F2 are exactly siblings ofci:-titled commits whose actual scope was narrower than the bug class —950e376only editedjust check-all,bd1f725only added an eslint config file. CI workflows were never edited.unsafe,dbg!/eprintln!/todo!/unimplemented!,Arc<Mutex>/Arc<RwLock>w/o lock-ok,panic!/unwrapin lib prod,js_sys::eval/innerHTML,TODO/FIXME/HACK,anyhow::in pure libs, vector caps in wire types,let _ = h.<method>().awaitswallow (per general-audit lessons: 2026-04-28 #477 lesson), CSP, docker pin/USER hardening, SQL prep statements, voice cap enforcement, identity malformed-bytes tests.Major themes since last audit
docs/specs/2026-04-27-event-based-waits-design.md):crates/web/src/test_hooks/(629 LOC, 4 files: dispatcher / mod / snapshot / wire), gated behindtest-hooksfeature with explicitdefault = [],WillowTestHooksJS interface for Playwright event-based waits.crates/clientexposes test-only address getters via the same feature gate.595b5ab):MAX_VOICE_CHANNELS = 256,MAX_PARTICIPANTS_PER_CHANNEL, voice-join gated on known channels. Tests added covering unknown-channel-drop + at-cap rejection.1a9503f):ProfileState.namesandNetworkMeta.typing_peersLRU-capped at 10k, TTL sweep on typing_peers (5s) piggy-backed on existing 1Hz presence-tick driver.crates/client/src/tests/voice.rs(6 tests),crates/client/src/tests/governance.rs(5 tests),crates/client/src/tests/actions.rs(covered by PR test(client): cover actions.rs translation logic #483).ZeroizeOnDroptriggered synchronously. Production callers: zero (RatchetCache itself only used in tests). See "Findings deemed not actionable" below.default-src 'self',script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval',frame-ancestors 'none', etc.'unsafe-eval'weakens but is required by existingjs_sys::evalsites ([WS-1] Web: js_sys::eval(format!()) for pinned-message scroll uses band-aid sanitization #425 [WS-1]).[](no STUN), opt-in viawindow.__WILLOW_STUN_URLS.warn_and_toast_within components (PR fix(web): route component handler errors through warn_and_toast_with #480): closes F2 from general-audit: main @ 958e1ec (2026-04-28) #474. Verifiedlet _ = h.<method>().awaitzero hits acrosscrates/web/src/components/,handlers.rs,app.rs.*State→*Signalsrename (refactor(web): rename ServerState/ChatState/VoiceState -> *Signals (#261) #471), prune dead_code allows (chore: prune #[allow(dead_code)] cluster (#327) #470), state tests split into per-concern files (refactor(state): split tests.rs monolith into per-concern files #486, 6 files now from one monolith).Concerns audited
apply_eventenforces)'unsafe-eval'documented), test-hooks privacy guard exists but F1 founddocs/specs/2026-04-26-state-management-model-design.md(12 sites, all annotated)Strong areas (re-verified, no findings)
crates/state/srcpurity: zerotokio/iroh::/std::fs/SystemTime(rg "tokio::|std::fs|SystemTime|iroh::" crates/state/srcclean)crates/{state,transport,identity,messaging,crypto,common}/srcMAX_DESER_SIZE = 256 KB+ per-variantWireMessage::max_sizeMAX_EVENT_DEPS = 64,MAX_ENCRYPTED_KEY_BYTES = 128MAX_PROFILE_NAMES = 10_000,MAX_TYPING_PEERS = 10_000, LRU evict, 5s TTL sweepMAX_VOICE_CHANNELS = 256, voice-join gated on known channel iddag.rs(708) +materialize.rs(1916) +permissions.rs(1087) +stress.rs(387) +sync.rs(247) +voting.rs(457) = 4802 LOCcrates/actor/src/addr.rs:91Sync for Addr,crates/web/src/state_bridge.rs:98Send for DerivedStateActor)unimplemented!()/todo!()/dbg!()/eprintln!()/FIXME/HACKin lib productionjs_sys::evalsites: 1 user-input-interpolated ([WS-1] Web: js_sys::eval(format!()) for pinned-message scroll uses band-aid sanitization #425 [WS-1] tracked), rest static-string — no new XSS surfacerust:1.95-slim-bookworm,nginxinc/nginx-unprivileged:1.27-alpine, all containers runUSER willow/USER nginx, trunk pinned0.21.14let _ = h.<method>().awaitzero hits across web (F2 from general-audit: main @ 958e1ec (2026-04-28) #474 properly fixed)crates/storage/src/store.rs: parameterized (?N), values passed viaBox<dyn ToSql>— no concatenation injection surfacedefault = [],WillowTestHookssymbol absent from prod under spec — but enforcement gap, see F1Findings (2 child issues)
scripts/check-no-test-hooks-in-prod.shsymbol-leak guard not wired into CI / deploy workflows — onlyjust check-alleslint.config.jsbanswaitForTimeoutbut no CI step or npm script runs eslintBoth findings = sibling-of-closed pattern. The
ci:-prefixed commits (950e376,bd1f725) only edited localjustrecipe / config file but never touched.github/workflows/.Findings deemed not actionable (verified during sweep)
ChannelKeydirectly incrates/client/src/state.rs::ServerEntry.keys: HashMap<String, ChannelKey>, dropped vialeave_server(crates/client/src/servers.rs:79) which triggersZeroizeOnDropper-entry. So there's no leak today, the API is precautionary library surface. Borderline tech-debt ("dead code in lib?") butRatchetCacheis documented public crypto primitive — not dead, just not yet adopted. Leave as-is.'unsafe-eval'weakens script-src. Required by existingjs_sys::evalsites (theme bootstrap, focus shim, pinned-message scroll). Tracked under [WS-1] Web: js_sys::eval(format!()) for pinned-message scroll uses band-aid sanitization #425 [WS-1].RUSTSEC-2026-0098,2026-0099,2026-0104no longer matched by current advisory DB scan). Cosmetic — won't break anything, but ignore list could be pruned. Not worth its own issue.waitForTimeoutcalls remain in 6 e2e/ files — already tracked in e2e: migrate remaining specs to event-based waits #458 as migration follow-up.#[allow(clippy::too_many_arguments)]sites — same 7 production sites as [tech-debt] Leptos components carryclippy::too_many_argumentsallows — extract Props structs #195; [tech-debt] Leptos components carryclippy::too_many_argumentsallows — extract Props structs #195 still tracked.Existing-issue overlap (not re-filed)
js_sys::eval(format!())for pinned scroll) → [WS-1] Web: js_sys::eval(format!()) for pinned-message scroll uses band-aid sanitization #425anyhowin lib crates) → [TD-14] anyhow used in 8 library crates contradicts CLAUDE.md convention #332 (verified zero hits in 6 pure-lib crates today; need to recheck network/client)ProfileState.names/ChatMetaState.typing_peersaccept unbounded attacker-supplied strings #234 — landed via1a9503fTopicAnnounce.topics: Vec<String>has no element-count cap; enables relay CPU amplification + topic-slot exhaustion #235 (landed via 3ee8a98)RotateChannelKey.encrypted_keys+Event.depsvectors have no element caps #236 (landed via 7ee7e0a)convert_case3-way split) → structural, [TD-02 follow-up]convert_case3-way split is structural — pinned by Leptos internals + derive_more #485getrandom3-way split) → structural, [TD-03 follow-up]getrandom3-way split is structural — driven by aes-gcm 0.10, ahash, and iroh 0.98 #481randmajor versions + RUSTSEC-2026-0097 across all of them #246 [DEP-03] Workspace uses unmaintainedbincode 1.3for on-wire + on-disk serialization #247 [DEP-08] Unmaintainedpaste 1.0.15(RUSTSEC-2024-0436) — not in CI ignore list #316 [DEP-09] Unmaintainedproc-macro-error 0.4.12(RUSTSEC-2024-0370) — not in CI ignore list #317 [DEP-10] Unmaintainedatomic-polyfill 1.0.3(RUSTSEC-2023-0089) — not in CI ignore list #318Method change vs #474
Lessons follow-up issue describes outcome.