# Ultra Fuzz Report — MNW Server (Run #9 — launch eve) **Run date:** 2026-05-31 (evening) **Run number:** 9 (launchplan_final.md §1.5 referred to it as "Run #5" — stale; this is the 9th) **Trigger:** launchplan §1.5 pre-launch pass ## Run #9 headline Run #8 closed with "BAR MET — ALL FIVE AXES A-". Run #9 went deeper and surfaced 1 CRITICAL + 4 SERIOUS + several MED/HIGH items the prior 8 runs missed. All four launch-critical items fixed in-session; remaining items deferred with rationale below. | Axis | Run #8 | Run #9 | Direction | |------|--------|--------|-----------| | Payments | A- | A- | flat — 2 new SERIOUS surfaced; 1 fixed (webhook unmark on dual-failure 503), 1 deferred (subscription out-of-order webhook) | | Storage | A- | A- | flat — 1 new HIGH (migration 129 dead-letter table unused) + 2 MEDs (is_s3_key_live unindexed full-scan, LIKE-suffix false-positive); deferred | | UX Wiring | A- → B- → A- | A- | dipped on grade-cap for signup TOCTOU CRITICAL, restored after fix | | Security | A- | A- | flat — 2 new SERIOUS, both fixed (JWT-bump non-atomic, 2FA email IP spoofable) | | Performance | A- | A- | flat — 2 new HIGH (per-request reqwest::Client::new in 5 hot paths, unbounded spawn in expired-account cleanup); deferred to post-launch | **Net Run #9 (post-fix):** 0 CRITICAL · 1 SERIOUS open (Payments subscription ordering — documented deferral) · 3 HIGH open (deferred) · 7 MED open (deferred). **Launchplan §1.5 A- bar holds.** ## Run #9 — CRITICAL fixed in-session ### UX-CRITICAL — Signup TOCTOU: race → 500 + form loss → FIXED 2026-05-31 `src/routes/pages/public/join_wizard.rs:99-139`. The wizard ran separate `get_user_by_username` / `get_user_by_email` checks before `create_user`. A concurrent signup with the same username or email slipping between SELECT and INSERT raised a 23505 unique violation that bubbled to `AppError::Database` → 500 "Something went wrong" — and the user's entire typed-in form was lost. On a public alpha-launch surge this is the highest-traffic public endpoint; the wrong page to be returning 500s on. **Fix landed:** `create_user` call site now matches `AppError::Database(sqlx::Error::Database(_))` with code 23505, inspects the constraint name (`users_username_key` / `users_email_key`), and routes through `return_error(..)` with a friendly message — same flow as the explicit pre-check branches. Same shape as the existing 23505 handling in `db/license_keys.rs`, `db/builds.rs`, `routes/api/guest_checkout.rs`. **Known follow-up (not blocking):** the form-reload still loses typed values on the error swap; `return_error` renders `LoginErrorTemplate` (message-only). Preserving field values would require threading them through the template — file a separate Phase 4 polish item. ## Run #9 — SERIOUS fixed in-session ### Sec-SERIOUS — `delete_all_sessions_for_user` non-atomic JWT bump → FIXED 2026-05-31 `src/db/sessions.rs:247-263`. The function ran `DELETE FROM user_sessions` then a separate `UPDATE users SET jwt_invalidated_at = NOW()` on independent connections. If the UPDATE dropped (pool timeout, conn drop, postgres restart), session cookies were dead but every outstanding SyncKit JWT survived until natural expiry — exactly the leak this function exists to prevent. The in-code comment ("a session row deleted without a JWT bump is harmless, the converse would leak access") inverted reality. **Fix landed:** both writes wrapped in `pool.begin()` / `tx.commit()`. Comment updated. ### Sec-SERIOUS — 2FA login-notification email uses spoofable IP → FIXED 2026-05-31 `src/routes/pages/public/two_factor.rs:308-312`. The 2FA-completion path read `x-forwarded-for` raw (first-comma-split) for the new-login email's IP field. Every other login surface (`routes/auth.rs:242`, `auth.rs:486`, `auth.rs:528`) routes through `crate::helpers::extract_client_ip` which prioritizes `CF-Connecting-IP`. An attacker who already captured a password could pre-set `X-Forwarded-For: 1.2.3.4` on the verify-2fa POST so the "new login from " email lied about origin — the exact email users are told to trust for compromise detection. **Fix landed:** swapped to `crate::helpers::extract_client_ip(&headers)`. One-line change, parity restored. ### Pay-SERIOUS — Webhook dual-failure dropped events silently → FIXED 2026-05-31 `src/routes/stripe/webhook/mod.rs:73-89`. Dedup row was marked processed before handler dispatch (correct for at-least-once). On `(handler_err, insert_failed_event_err)` dual failure, code returned 503 to trigger Stripe redelivery — but Stripe's redelivery would short-circuit at the dedup check (line 50) and 200 the event without ever processing it. The code's own comment acknowledged the bug; the right tool (`unmark_event_processed`, defined 30 lines away in `db/webhook_events.rs:40`) was never called. **Fix landed:** call `db::webhook_events::unmark_event_processed(&state.db, &event_id)` before returning 503, with logged-error best-effort if even that fails (same scenario where 503 was already wrong). ## Run #9 — DEFERRED with rationale (above A- bar) ### Pay-SERIOUS — Subscription webhook out-of-order events resurrect `active` `src/routes/stripe/webhook/subscriptions.rs:90, 116, 140`. Handlers blindly overwrite `subscriptions.status` and `period_end` from the webhook payload. Stripe does NOT guarantee delivery order. Sequence `past_due → active` reordered as `active → past_due → active(stale)` overwrites a legitimate `past_due` with stale `active` — restoring access for a user who hasn't paid. **Deferral rationale:** worst case is restored access for a few minutes until the next webhook arrives. Fix requires re-extracting Stripe's top-level `created` from `UntypedEvent` (currently dropped) and adding `WHERE last_event_at IS NULL OR last_event_at <= $created` guards on every status/period write across Fan+, creator-tier, and synckit code paths — non-trivial cross-cutting change. Post-launch fix in Phase 4; tracked in todo.md. ### Sto-HIGH — Migration 129 dead-letter table never written `migrations/129_pending_s3_deletions_dead_letter.sql` creates `pending_s3_deletions_dead_letter` and documents it as "operator-visible parking lot... require manual triage." `src/scheduler/cleanup.rs:453-457` on `attempts >= 10` only logs `tracing::error!` then removes the row — never inserts into the dead-letter table. Permanently-failing keys have zero operator visibility. **Deferral rationale:** operational, not runtime. No user impact; only operators lose triage signal. One-INSERT fix; bundle into Phase 4. ### Perf-HIGH — Per-request `reqwest::Client::new()` in 5 hot paths `routes/pages/dashboard/main.rs:118`, `routes/pages/public/landing.rs:284`, `routes/api/internal/cli_features.rs:440`, `routes/api/domains.rs:319`, `auth.rs:559`. Each call builds a fresh TCP pool, TLS context, DNS resolver — no keep-alive across requests. `MtClient` in `AppState` already keeps a pooled client; the dashboard bypasses it. **Deferral rationale:** real but matters at scale. Private alpha launch traffic well below where this becomes a tail-latency contributor. 30-min refactor; bundle into Phase 4 once launch traffic settles. ### Perf-HIGH — Unbounded `tokio::spawn` in expired-account cleanup `src/scheduler/cleanup.rs:215-220` (`spawn_expired_account_cleanups`). Daily tick spawns one task per expired account, no governor. `cleanup_sandbox_accounts` (same file, ~100 lines above) correctly caps at `CLEANUP_PARALLELISM=4` via `JoinSet`; the terminated/content-removal variants don't. A backlog of 200 expired accounts fan-outs 200 concurrent S3 prefix listings racing for the 25-conn pool at midnight. **Deferral rationale:** runs once daily; current expired-account count is small (private alpha). Trivial fix (lift the existing JoinSet pattern); not launch-blocking. Bundle with Phase 4. ## Run #9 — MED/LOW deferred (read-only carry-forward, in todo.md) - Pay-MED: `pricing.rs::parse_dollars_to_cents` misinterprets European decimal comma (`1,23` → 12300¢). User-controlled input; fixable in a single regex. - Pay-MED: SyncKit app-sub checkout silently defaults `storage_limit_bytes` to 0 if metadata missing. - Pay-MED: Guest checkout email falls back to `"unknown@guest"` sentinel; collisions possible. - Sto-MED: `is_s3_key_live` runs 7 EXISTS subqueries on unindexed `items.audio_s3_key` / `cover_s3_key` / `video_s3_key` / `versions.s3_key` etc — sequential scans per retry. - Sto-MED: `is_s3_key_live` LIKE-suffix pattern `'%' || s3_key` false-positives on neighboring keys (key `abc/file.png` matches `xabc/file.png`) — skips a legitimate delete → S3 object leaks. - UX-MED: "Log in" return_to query param in `purchase.html:145` is dead-wired — login handler always redirects `/dashboard`. Lost purchase intent. - UX-MED: Admin user filter buttons (`admin-users.html:35-44`) use `class="primary"` / `class="secondary"` instead of `btn-primary` / `btn-secondary` — renders unstyled. - UX-LOW: Pagination links in `git/issues.html:72,76` don't URL-encode `search`; `&page=99` in search query corrupts pagination. - UX-LOW: 5 sites do `.render().unwrap_or_default()` on Askama templates (blank UI on render failure, no log). - UX-LOW: `slugify` in `formatting.rs` produces `"post"` for any non-ASCII title; international creators get opaque URLs. - Sec-MINOR: `csrf.rs:176-185` `validate_token_consuming` doesn't consume — name promises stronger property than implementation. - Sec-MINOR: `routes/oauth.rs:101-111` `is_localhost_redirect` allows any port on localhost regardless of registered URI. - Sec-MINOR: `routes/pages/public/two_factor.rs::pending_2fa_started_at` reads `i64` via session.get; type mismatch silently → None → instantly-expired. - Sec-MINOR: `scanning/archive.rs:124` path-traversal check misses lone `..` segment (no trailing separator). - Perf-LOW: `scheduler/announcements.rs` linear walk through subscriber list in a single spawned task; no checkpointing. - Perf-LOW: `db/page_views.rs` `pending` HashMap has no max-cardinality cap (crawler hitting 100k unique target_ids before tick). - Perf-LOW: `build_runner.rs:441` local artifact tmpfile leaks if process crashes between SCP and `remove_file`. ## Run #9 — mandatory surprises - **Payments:** `routes/stripe/webhook/mod.rs:82-89` literally documents the bug it ships ("the dedup row was already marked processed... Stripe won't retry") and then chooses 503 anyway. The fix (`unmark_event_processed`) sat 30 lines away in the same crate, never called. Scar-tissue-comment-without-the-fix is a recognizable pattern across the codebase. - **Storage:** `routes/storage/mod.rs::commit_upload` sealed-helper pattern (Run #7 fix for the chronic disease) is the strongest piece of structural engineering in the repo — turned an enum into a witness type. But the *neighbor* file `migrations/129_pending_s3_deletions_dead_letter.sql` shows the opposite: migration written with detailed prose explaining the operator's parking lot, and the actual INSERT never wired up. Two adjacent fixes from the same audit-cycle, one structural and load-bearing, one ceremonial and silently broken. - **UX:** `csrf.rs` `PostureMethodRouter` + sealed `CsrfManuallyValidated` witness make registering a mutation route without an explicit posture declaration *uncompilable*. A+ engineering. The contrast with the signup wizard's TOCTOU-and-500-with-lost-form is jarring — defensive depth on CSRF, none on the front door. - **Security:** `routes/auth.rs:128-130` malformed-email branch skips the DUMMY_HASH timing equalizer that was added explicitly to prevent timing-side-channel user enumeration. ~2 orders of magnitude faster than every other failure path. The equalizer exists; this one path bypasses it. - **Performance:** `db/projects.rs::get_project_ids_for_user` is the only `fetch_all` in `projects.rs` without a `LIMIT`. Its neighbor `get_projects_by_user` caps at 500 with a documented safety comment. Cyber-squatter with 10k projects + account expiry → 10k S3 prefix-deletes in one spawned task. Asymmetric defense within the same module. ## Run #9 — stress-tested OK Verified attacks the code survived (high-confidence positives): - Stripe webhook signature replay (HMAC constant-time, multi-secret rotation, timestamp tolerance both directions) - Promo code concurrent over-use (single atomic UPDATE with max_uses + expires_at + starts_at) - Cart race past pre-check (23505 fallback aborts cleanly without charging) - License key prediction (6 wordlist × CSPRNG ≈ 66 bits) - Pre-signed URL Content-Length binding (S3 rejects mismatch at protocol level) - Storage cap atomicity (`try_replace_storage` single UPDATE) - Build claim race (partial unique index + 23505 backstop) - Idempotent re-confirms in all 4 upload confirm handlers (reaper-deletes-live-object closed) - Session row + JWT atomicity (post-fix verified above) - TOTP replay across skew window (matched-step tracked + strict `>` gate) - OAuth PKCE downgrade (S256 pinned at authorize + token-exchange) - CSRF body bypass via textarea-smuggled token (proper form parser) - Git diff/blame XSS (HTML-escaped in attacker-controlled spots) - Internal error leakage (tests assert no PG host, no S3 bucket, no sqlx variant leaks) ## Run #9 confidence per axis - Payments **HIGH** (~70% LoC read this pass; Phase 4 backlog visible) - Storage **HIGH** (full module read; cleanup.rs upper half only — MEDIUM there) - UX Wiring **HIGH** for CSRF/error/validation; **MEDIUM** for wizard step partials, embed routes, dashboard CSV import - Security **HIGH** for auth/CSRF/session; **MEDIUM** for scanning (YARA rule content unread), API key scoping - Performance **HIGH** for scan worker, scheduler, storage, build_runner; **MEDIUM** for SyncKit, postmark, import pipeline ## Run #9 bug counts | Severity | Payments | Storage | UX | Security | Perf | Total | |---|---|---|---|---|---|---| | CRITICAL | — | — | 1 (FIXED) | — | — | **1** | | SERIOUS | 2 (1 FIXED, 1 deferred) | — | — | 2 (FIXED) | — | **4** | | HIGH | — | 1 (deferred) | — | — | 2 (deferred) | **3** | | MED | 3 (deferred) | 2 (deferred) | 2 (deferred) | — | — | **7** | | LOW/NOTE | 2 | — | 3 | 4 | 3 | 12 | ## Run #9 delta vs Run #8 - 1 CRITICAL surfaced + fixed (signup TOCTOU); class missed by prior 8 runs because no agent explicitly probed the public-signup race window - 4 SERIOUS surfaced; 3 fixed in-session, 1 deferred with rationale - Run #8 "BAR MET" claim was correct *for the surfaces it audited* but understated: this pass added explicit attack-vector probing for cross-conn atomicity, IP spoof parity across auth surfaces, and webhook dedup edge paths — none of which were in prior runs' scope - All previously closed Run #8 fixes verified intact (commit_upload seal, S1 tx atomicity, background.rs queue, cart MEDs) --- # Ultra Fuzz Report — MNW Server (Run #8 — historical) **Run date:** 2026-05-31 **Run number:** 8 ## Run #8 Headline | Axis | Run #5 | Run #6 | Run #7 | Run #8 | Direction | |------|--------|--------|--------|--------|-----------| | Payments | B | B+ | A- | **A-** | flat — H2 still deferred; 2 new MEDs surfaced (cart `min_price_cents` bypass, cart-all chain-break on all-free first seller) | | Storage | B- | A- | B+ | **A-** | ↑ H1 + S1 fixes verified closed; commit_upload seal intact across all 7 confirm handlers; genericization clean at every caller including synckit/blobs.rs | | UX Wiring | B | A- | A- | **A-** | flat — 1 new MED (item-wizard `pricing_model` silent fallback to "free" — same disease class fixed in project wizard at Run #6, not propagated) | | Security | A- | A- | A- | **A-** | flat — only diff in scope (username availability fail-closed) is a net improvement; MED backlog identical to Run #5/#6/#7 | | Performance | B- | A- | A- | **A-** | flat with 1 new SERIOUS — webhook `checkout_helpers.rs` unbounded `tokio::spawn` (send_purchase_emails / mailing_list / tip_email) competes with request handlers for the 25-slot pool under burst | **Net Run #8:** 0 CRITICAL · 1 SERIOUS new (Perf webhook spawn) — FIXED 2026-05-31 · 5 new MED — ALL FIXED 2026-05-31 · 1 SERIOUS previously-deferred (Payments H2 `claim_free_project` soft race) — FIXED 2026-05-31. **Post-Run #8 status (2026-05-31 end-of-day): 0 CRITICAL · 0 SERIOUS · 0 MED open from any prior run.** All five axes A-, all above-MED items closed, all Run #8 MEDs closed, prior-deferred SERIOUS closed. Launchplan §1.5 bar fully cleared. **2026-05-31 post-Run-#8 backlog sweep (7 waves):** 24 of 26 carried MED/LOW/NOTE items closed across Storage (5), Security (8), Performance (3), UX (2), Payments (2), Auth (4). Two deferred with rationale: `build_runner.rs` serial targets (LOW, builds run rarely, refactor touches denominator) and `scheduler/mod.rs` advisory-lock granularity (multi-replica concern, single-process today). New schema migration `133_items_duration_seconds_nonnegative.sql` pins the negative-duration invariant in the DB. New `commit_rescan` helper extends the chronic-disease commit_upload seal to admin paths. Tests: 1655 / 0. **Launchplan §1.5 bar:** **ALL 5 AXES AT A- — BAR MET.** The new Perf SERIOUS is axis-internal and the agent kept Perf at A- (machinery wins outweigh; same shape as previously-closed `record_view` per-request spawn — apply mpsc + drainer pattern). New Payments MEDs and UX MED are launch-quality items worth addressing or documenting before ship; none are A- blockers. ## Run #8 — new findings above MED ### P-SERIOUS — Webhook hot-path unbounded `tokio::spawn` (Performance) — FIXED 2026-05-31 `src/routes/stripe/webhook/checkout_helpers.rs:58, 96, 124, 290` + `src/routes/stripe/webhook/checkout.rs:618`. `send_purchase_emails`, `subscribe_buyer_to_mailing_list`, `send_tip_email`, `send_guest_sale_notification`, guest-purchase-confirmation each `tokio::spawn` from the webhook handler. Multi-item cart fires N spawns per webhook; each task acquires 1-2 pool conns + a Postmark call. No JoinSet, no cap. Under burst, hundreds of detached tasks competed with request handlers for the 25-slot pool. Same shape as the Run #4 `record_view` per-request spawn (fixed via mpsc + drainer). **Fix landed:** new generic `src/background.rs` module — `BackgroundTx` + `spawn_pool()` with bounded mpsc (capacity 1024) + semaphore-bounded concurrent execution (8 workers, well below `DB_POOL_MAX_CONNECTIONS=25`). `state.bg.spawn(name, fut)` is non-blocking; queue overflow logs a warning and drops the task. The `spawn_email!` macro was refactored to use the bg queue (covers 17 callers across auth/admin/follows/library/two_factor/stripe webhook/login flows). The 5 manual webhook `tokio::spawn` sites were also migrated. Per-request email sends from postmark issue replies (×2), guest-claim email, and join-wizard signup (×2) were migrated in the same pass — same disease, same fix. **Out of scope for this fix** (different bug shapes; defer to Phase 4 polish or own remediation): import pipeline (long-running, needs own bound), MT community creation (single outbound HTTP, minor pool pressure), creator departure notification + status broadcast (broadcast-class — use `broadcast.rs` JoinSet pattern), idempotency-store post-response (trivial DB write), build_runner (already gated by claim flow), scheduler/monitor/scanning/page_views (background workers, not per-request). ### Payments MED — Cart `min_price_cents` bypass — FIXED 2026-05-31 Both cart paths (`process_seller_checkout` and `create_cart_checkout`) now check `pc.min_price_cents` for non-platform Discount codes before applying the discount. Cart skips the ineligible item (others may still qualify) rather than rejecting the whole cart — matches the existing scope-skip pattern. ### Payments MED — Cart-all chain-break on all-free first seller — FIXED 2026-05-31 `process_seller_checkout` signature changed `Result` → `Result>`; all-free path now returns `Ok(None)` instead of `Err(BadRequest)`. New `drain_to_paid` helper loops through the queued sellers until a paid one is reached (returns URL) or queue exhausted (returns `Ok(None)` → library redirect). Both callers (`create_cart_checkout_all` and `checkout_success`) updated to use it. ### UX MED — Item wizard `pricing_model` silent fallback — FIXED 2026-05-31 `save_pricing` now rejects missing pricing_model with `AppError::validation("Select a pricing model")` and rejects unknown values with `format!("Unknown pricing model: {other}")`. Same shape as the project wizard Run #6 fix. ### UX MED — Inline-JS template duplication — FIXED 2026-05-31 Added delegated `data-copy-link` click handler to `static/mnw.js` with proper `.catch()` (falls back to `window.prompt` in non-secure contexts — better than the silent-no-op the inline snippets shipped with). 8 templates migrated from `onclick="navigator.clipboard.writeText(...).then(...)"` to `Copy link` (audio_player, blog_post, collection, item, project, text_reader, user, video_player). `href` is the real URL so middle-click / no-JS / share menus still work. Cache-bust query bumped to `v=0531`. ### Perf MED — Cart free-claim N+1 — FIXED 2026-05-31 Extended `CartItem` with `enable_license_keys` + `default_max_activations` (both cart queries pull them through). Three free-claim loops (single-seller paid path, discount-zeroed promo path, chain-flow path) drop the per-item `get_item_by_id` and replace per-item `remove_from_cart` DELETE with a single bulk `remove_from_cart_bulk(..., ANY($2))` at the end of each loop. Per-item tx for `claim_free_item` stays (the per-item claim-vs-already-purchased return value is load-bearing for sales-count increment). Roundtrips per free item dropped from ~5-7 to ~3-4; per-loop DELETEs from N to 1. ## Run #8 — verified standing (storage fixes from session) - **H1** (`uploads.rs::confirm_upload` L295-337) — three-arm match correct. Zero-rows arm rolls back (replace path = `try_replace_storage` swap-back with `i64::MAX` cap; fresh-upload path = `decrement_storage_used`), then `enqueue_s3_orphan(new_key)`, returns BadRequest "Item was modified concurrently." Returns BEFORE `commit_upload` and BEFORE `remove_pending_upload` — pending_uploads row left as reaper second-line defense. - **S1** (`media.rs::media_confirm` L241-293) — single `state.db.begin()` wraps storage credit + pending_uploads clear + media_files INSERT. S3 IO entirely outside tx. tx drop → Postgres ROLLBACK → all three writes reverted atomically. 23505 detection via typed `AppError::Database(sqlx::Error::Database(...))` pattern works post-rollback. S3 cleanup fires on every tx-failure branch. - **Genericization** — `pending_uploads::remove_pending_upload` and `media_files::create` now `impl PgExecutor<'e>`. All 12 callers (including `synckit/blobs.rs:157`) still compile and execute correctly. - **Pool pressure delta from S1 tx** — neutral-to-better. Prior code grabbed 3 separate conns serially; new code grabs 1 conn for ~3× the duration. Users-row write lock held ~ms. Per-user serialization for sub-second uploads acceptable. ## Run #8 — mandatory surprises - **Payments:** `compute_splits` more careful than its comment promises — remainder-distribution loop constrained by `expected_total = amount * raw_total_pct.min(100) / 100`, so under-100% splits keep the owner's share AND distribute floor-rounding remainders up to bound. Proptest-style invariant tests fully fence it. - **Storage:** `try_increment_storage_on` inside the tx holds a row-level lock on `users` for the duration of the tx. Not a bug (sub-ms hold; cap can't be over-shot via WHERE re-evaluation under READ COMMITTED). But every media confirm now serializes per-user against every other storage write. - **UX:** Copy-link button is a chimera. Nine templates copy the same inline `onclick` that calls `navigator.clipboard.writeText`, mutates `this.textContent` to `"Copied!"` — silently broken in any tab loaded over plain HTTP, in iframes, or with restrictive CSP. No `.catch()` → no fallback, no error. - **Security:** `routes/auth.rs:128-130` malformed-email branch skips DUMMY_HASH timing equalizer. ~2 orders of magnitude faster than every other failure path — distinguishes "you submitted an invalid-email-shaped string" from "valid email, unknown account." Real timing oracle a few lines above the equalizer that was deliberately added to prevent exactly this. - **Performance:** `metrics::idempotency_middleware` does a DB SELECT on EVERY POST/PUT with an `Idempotency-Key` header BEFORE the handler runs. No bloom filter, no negative cache. ~1 extra ms per POST already doing 2-5 DB queries — free 20%+ on POST p50 available by adding an in-memory `seen` set. ## Run #8 bug counts | Severity | Payments | Storage | UX | Security | Perf | Total | |---|---|---|---|---|---|---| | CRITICAL | — | — | — | — | — | **0** | | SERIOUS | 1 (deferred) | — | — | — | 1 (new) | **2** | | MED | 2 (new) | 7 | 5 | 8 | 5 | 27 | | LOW/NOTE | 5 | 3 | 4 | 3 | 2 | 17 | ## Run #8 confidence per axis - Payments **HIGH** (~70% LoC read) - Storage **HIGH** (full) - UX **HIGH** - Security **HIGH** (scoped); MEDIUM for storage-route auth side-effects - Performance **HIGH** ## Run #8 delta vs Run #7 - **Storage B+ → A-.** H1 + S1 fixes verified closed. Genericization clean. - **Payments A- flat.** 2 new MEDs (cart `min_price_cents` bypass, cart-all chain-break) surfaced via expanded coverage; H2 deferred unchanged. - **UX A- flat.** 1 new MED (item-wizard `pricing_model` silent fallback) — same disease class as project wizard fix from Run #6, not propagated. - **Security A- flat.** Net improvement (username fail-closed). MED backlog identical. - **Performance A- flat.** 1 new SERIOUS (webhook unbounded spawn) — same shape as Run #4 `record_view` fix. Cart free-flow N+1 (MED) — Run #5 fix covered paid only. --- # Ultra Fuzz Report — MNW Server (Run #7 — historical) **Run date:** 2026-05-31 **Run number:** 7 (+ S1 + Storage code-fuzz fixes confirmed in Run #8) ## Headline | Axis | Run #5 | Run #6 | Run #7 | Direction | |------|--------|--------|--------|-----------| | Payments | B | B+ | **A-** | ↑↑ Phase 2 + Run #6 + Run #7 fixes all landed; S1 cart 23505 swallow fixed post-Run #7; H2 claim_free_project soft race deferred | | Storage | B- | A- | **B+ → A- pending Run #8** | ↑/↓ commit_upload structural fix is excellent; Run #6 idempotency fix introduced HIGH-1 (pending_uploads leak in 4 sites) + HIGH-2 (missing rollback on update_*_url) — both fixed post-Run #7. Storage code-fuzz 2026-05-31 surfaced H1 (confirm_upload silent zero-rows + side-effects-already-fired) and reopened S1 media_confirm tx atomicity — both fixed in same session | | UX Wiring | B | A- | **A-** | ↑ field-aware deletion + parse_dollars_to_cents shared; pricing_model silent fallback HIGH found and fixed post-Run #7 | | Security | A- | A- | (unchanged) | flat — no security-touching changes in Runs #6/#7 | | Performance | B- | A- | (unchanged) | flat — no perf-touching changes in Runs #6/#7 | ## Post-Run #7 Storage code-fuzz (2026-05-31) Targeted code-fuzz scoped to the Storage axis to verify A- before triggering full Run #8. Two findings above MED, both fixed in-session: - **H1 (HIGH) — `routes/storage/uploads.rs::confirm_upload` silent `rows_affected = 0`.** Same shape as the just-closed HIGH-2 (`update_*_url`), one step further along the same handler family. UPDATE at L295 uses ownership-filter `WHERE id = $1 AND project_id IN (SELECT id FROM projects WHERE user_id = $4)`; `rows_affected()` was never checked. If the item was deleted between `get_item_owner` (L156) and the UPDATE, storage credit stayed incremented, `pending_uploads` got cleared a few lines down, and `commit_upload` enqueued a scan job against a ghost target — permanent S3 leak + over-charged counter. **Fix:** three-arm match on the UPDATE result; zero-rows case rolls back storage and routes the new S3 key through `enqueue_s3_orphan` so the reaper still cleans it, then returns BadRequest "Item was modified concurrently." - **S1 (SERIOUS, Run #5 plan #12 reopened) — `routes/storage/media.rs::media_confirm` three-write atomicity.** Run #5 called for wrapping `try_increment_storage` → `remove_pending_upload` → `media_files::create` in a transaction; Run #7's in-process compensation only covered in-process errors. Process interruption (panic, OOM kill, container restart) between any two writes still leaked. **Fix:** all three writes now in a single tx; tx drop rolls back storage + pending_uploads + media_files atomically. Only the S3 object needs explicit cleanup (single `delete_object` after rollback). Supporting DB-layer changes: `creator_tiers::try_increment_storage_on(&mut PgConnection)` tx-friendly variant; `pending_uploads::remove_pending_upload` and `media_files::create` signatures genericized to `impl PgExecutor<'e>` (backwards compatible). Remaining storage MED/LOW (below launchplan §1.5 A- bar; ride into Phase 4 polish or document deferral): - MED — `update_project_image_url` / `update_item_cover` ignore `rows_affected()` (same shape as H1; mitigated for current callers because the only follow-on side-effect is `bump_cache_generation`). - MED — `downloads.rs:120` `((duration as u64) * 2).max(3600)` with no DB `CHECK (duration_seconds >= 0)`. Negative duration → multi-decade presigned URL. Exploitability requires creator-controlled negative duration; ffprobe doesn't produce them. Cap in code + add CHECK migration. - MED — Admin rescan paths (`routes/admin/uploads.rs:347, 390`) call `db::scan_jobs::enqueue` directly, bypassing the `commit_upload` structural seal. Ordering is correct so no live bug; demote `db::scan_jobs::enqueue` to `pub(crate)` and expose `commit_rescan(target, ...)` to close the chronic-disease finding for real. - MED — `enqueue_s3_orphan` single-policy doc in `routes/storage/mod.rs:24-30` overstates the discipline; many `s3.delete_object(...).await.ok()` direct calls remain at pre-storage-credit rejection paths. Tighten the doc or migrate the post-storage-credit sites. - MED — `is_s3_key_live` doesn't enumerate project image URLs (project cover keys live in a distinct prefix so no current bug; surface is fragile if future code paths queue project image keys). - LOW — `scanning/worker.rs:251` inline `UPDATE media_files SET scan_status` instead of `db::scanning::update_media_file_scan_status` helper. - LOW — `routes/pages/dashboard/wizards/item/save.rs:95` `update_item_cover_image_url` updates only `cover_image_url` (not s3_key/size); client-side hidden-field abuse can desync. - LOW — `db/pending_uploads.rs::remove_pending_upload` deletes by s3_key alone (per-handler prefix validation makes cross-user collision unreachable, but the function signature is broader than it needs to be). **Chronic disease status (5th run):** The invariant-in-prose / sibling-not-swept pattern that recurred across Runs #2–#6 was **structurally addressed** in Run #7 via two helpers: - `routes/storage/mod.rs::commit_upload(target: CommitTarget, ...)` — sealed `enqueue_scan_for` to module-private; the helper is now the only handler-reachable path for scan enqueue + scan_status flip after a DB write. Bug shapes 1–3 from prior runs are now structurally impossible to introduce in a new sibling. - `crate::pricing::parse_dollars_to_cents` + `validate_dollars_f64` — canonical dollar-to-cents conversion; bypassing has historically introduced NaN→$0 and saturating-overflow silent bugs. **Net after Run #7 + S1 fix:** 0 CRITICAL · 0 HIGH/SERIOUS · 1 SERIOUS deferred (Payments H2 soft race on `claim_free_project`) · a handful of MED/LOW polish items. --- # Ultra Fuzz Report — MNW Server (Run #5 — historical) **Run date:** 2026-05-30 **Run number:** 5 ## Headline | Axis | Run #4 | Run #5 | Direction | |------|--------|--------|-----------| | Payments | A- | **B** | ↓ (Run #4 plan items closed; 4 new SERIOUS surfaces previously unaudited: NULL item_id refund, splits >100% overflow, tip project authorization, cart unlisted bypass) | | Storage | A- | **B-** | ↓ (Run #4 `images.rs` ordering bug closed; same disease reappeared in `uploads.rs` route gate ordering — file-type rejection runs AFTER scan enqueue) | | UX Wiring | C+ | **B** | ↑ (Run #4 CSRF patchwork + creator-tier token fixed and structurally enforced; new CRIT: field-aware validation API is dead code at template boundary) | | Security | B+ | **A-** | ↑ (Run #4 git-shell validation, lockout email flood, CSRF policy all verified; no new CRIT/HIGH; remaining gaps are operational/MED) | | Performance | B | **B-** | ↓ (Run #4 scan_jobs retention + pool permit + broadcast bounding verified; new HIGHs in previously unaudited cart checkout + page-view paths + scheduler integrity scan) | Net: 3 CRITICAL (vs Run #4: 4), 13 HIGH/SERIOUS (vs Run #4: 10), 11 MED, 9 MINOR/LOW. Two axes regressed because Run #5 reached previously-unaudited territory (Payments tip/cart/refund edges; Performance hot-path request loops) while Run #4 plan items themselves were correctly closed. The Storage regression is a *recurrence of the same shape* in a sibling handler — the chronic invariant-in-prose disease, fourth consecutive run. ## Critical / High Findings (fix before launch) 1. **[Storage — CRITICAL]** `routes/storage/uploads.rs:204-237` — `confirm_upload` calls `enqueue_scan_for(...)` and `update_item_scan_status(... Pending)` BEFORE the match arm rejects `Download`/`Insertion`/`MediaImage`/`MediaVideo` with `BadRequest`. A misrouted-but-valid `item_id` confirms flips that item's scan status to Pending, blocks `stream_url` for every fan, and leaks a scan-job row for an S3 key that's then deleted. 2. **[UX — CRITICAL]** `error.rs:216-264` + `templates/error.html` — `AppError::validation_fields(summary, [(field, msg), ...])` is consumed only by unit tests. `ErrorTemplate` has no `fields:` member; no template renders per-field highlights. Every non-HTMX validation failure degrades to the global "Go Home / Go Back" page and wipes submitted form input. Handler authors are misled into thinking their carefully-tagged field errors reach the UI. 3. **[Perf — CRITICAL]** `build_runner.rs:175-180` — Partial-failure error message reports `("{}/{} succeeded", artifact_keys.len(), artifact_keys.len() + 1)`. Denominator is always `succeeded + 1`, regardless of how many targets actually ran. Three targets, one succeeded, two failed → reports "1/2" (should be 1/3). Failed-target count is never tracked. ### HIGH / SERIOUS 4. **[Payments — SERIOUS]** `db/transactions.rs:699-716` — `refund_transaction_by_payment_intent` returns `Vec<(TransactionId, ItemId)>` (non-Optional). Project-level transactions store `item_id IS NULL` (`routes/stripe/checkout/project.rs:135`). On `charge.refunded` for a project-level purchase, sqlx fails to decode NULL → `ItemId`; webhook handler 5xx's; Stripe retries forever. 5. **[Payments — SERIOUS]** `routes/stripe/webhook/checkout_helpers.rs:240-269` — `compute_splits` comment says "Defensive clamp: a misconfigured project_members row could sum past 100%" but the loop only adds remainder pennies and never subtracts. Two members at 60%+60% on $10 each are credited $6 each — $12 of $10 of revenue. Clamp only affects `expected_total`, never the already-computed per-member amounts. Tests cover ≤100% only. 6. **[Payments — SERIOUS]** `routes/stripe/checkout/tips.rs:104-106` — `TipForm.project_id` is taken verbatim from the form. The webhook later calls `record_tip_splits(tip.id, tip.project_id, ...)` and credits THAT project's members. An attacker tipping creator A can pass project B's UUID; B's members get split obligations credited against A's tip. Stripe money flows correctly; on-platform `tip_splits` records and any downstream reporting are corrupted. 7. **[Payments — SERIOUS]** `db/cart.rs:94-123` + `routes/stripe/checkout/cart.rs` — `item.rs:47-49` enforces "Unlisted items can only be obtained through their bundle" via `if !item.listed`. `toggle_cart_preflight` and `get_cart_items` check `is_public` but NOT `listed`. An attacker who knows an unlisted item's UUID can POST to `/api/cart/{id}/toggle` and check out via the cart flow, fully bypassing the bundle-only gate. 8. **[Payments — SERIOUS]** `routes/stripe/webhook/subscriptions.rs:117-121, 67-69, 95-96` — `status_str.parse::()` returns BadRequest for any status not in `enums.rs:183-198` (Stripe's `paused` is new). Webhook handler returns Err; scheduler retries forever until status changes. 9. **[Payments — SERIOUS]** `payments/webhooks.rs:294-308` — `is_full_refund` returns true when `amount_refunded >= amount` and both are zero (Stripe sometimes emits these for $0 verification charges). Triggers `refund_transaction_by_payment_intent` with default `unknown` intent ID. Test at line 517-525 pins the behavior. 10. **[Storage — HIGH]** `routes/storage/versions.rs:159-174` — `version_confirm_upload` enqueues scan and flips `scan_status` to Pending BEFORE the `version.s3_key == req.s3_key` idempotency check at line 172. Duplicate retry of an already-confirmed upload knocks a Clean version back to Pending, breaking downloads. 11. **[Storage — HIGH]** `routes/storage/images.rs:179-208` — `project_image_confirm` replace branch is gated on `Ok(Some(old_size))` from `s3.object_size(&old_key)`. On `Err` (S3 hiccup) or `Ok(None)` (URL with no object behind it) it falls into the "no old image" branch, `try_increment_storage` without decrementing. Permanent storage over-count. Also: `update_project_image_url` runs AFTER `enqueue_deletions` of the old key, with no rollback path. 12. **[Storage — HIGH]** `routes/storage/media.rs:236-293` — `media_confirm` does three separate writes (`try_increment_storage`, `remove_pending_upload`, `media_files::create`) outside a transaction. Interruption between steps leaves S3 object orphaned with storage credit consumed and no DB row. 13. **[UX — HIGH]** `routes/pages/dashboard/wizards/item/save.rs:183-185, 214-227` — `let price_cents = (price_dollars * 100.0).round() as i32; if price_cents > 0 { validate_price_cents(price_cents)?; }`. Guard skips validation for 0 and negative values; value goes through `PriceCents::from_db` (no validation) into `update_item`. Submitting `price=-5` writes `-500` cents. Same pattern on PWYW: no `min <= suggested` check. 14. **[UX — HIGH]** `routes/pages/dashboard/wizards/item/save.rs:179-183` + `routes/api/items/bulk.rs:136-139` + `routes/pages/dashboard/wizards/project.rs:264-298` — `price_dollars: f64 = …parse()…unwrap_or(0.0)`. `"NaN".parse::()` succeeds; `NaN as i32 == 0` (silent Free). `1e20` saturates `i32::MAX`. Bulk path catches via `PriceCents::new` cap; `save.rs` does not — persists raw. 15. **[UX — HIGH]** `routes/auth.rs:356-361` — `let is_taken = db::users::get_user_by_username(...).await.map(|u| u.is_some()).unwrap_or(false);`. Transient DB error during signup live-check returns "available", misleading the user; subsequent signup races whatever real state the DB is in. 16. **[Perf — HIGH]** `routes/stripe/checkout/cart.rs:68-248` — Per cart item: sequential `has_purchased_item`, optional `remove_from_cart`, per-free-item `begin tx → claim_free_item → increment_sales_count → commit`, `get_item_by_id`, second `remove_from_cart`. 20-item cart ≈ 80 sequential roundtrips, ~20 separate transactions, 20 distinct pool acquisitions in series. 17. **[Perf — HIGH]** `db/page_views.rs:18-32` — `record_view` spawned per public request, takes a pool connection to UPSERT. With `DB_POOL_MAX_CONNECTIONS = 25`, a viral item link spawns unbounded tasks, eats the pool, times out real request handlers at acquire. No batching, no per-(target,session) debounce. 18. **[Perf — HIGH]** `scheduler/integrity.rs:53-73` — `check_sales_count_drift`: `SELECT i.id, i.sales_count, COUNT(t.id) FROM items LEFT JOIN transactions ... GROUP BY i.id HAVING i.sales_count != COUNT(t.id) LIMIT 50`. `HAVING` post-aggregation; Postgres scans every row in `items` and joins every completed transaction in history before filtering. `LIMIT 50` doesn't cap the work. Weekly multi-minute query holding a pool connection. ## Scorecard ### Axis Summary Grades | Axis | Overall | Cold Spots | Mandatory Surprise | |------|---------|------------|--------------------| | Payments | B | `routes/stripe/checkout/cart.rs` (B-), `routes/stripe/checkout/tips.rs` (B-), `db/transactions.rs` (B-), `routes/stripe/webhook/checkout_helpers.rs` (B-), `routes/stripe/webhook/subscriptions.rs` (B) | `compute_splits` carries a "Defensive clamp" comment that explicitly anticipates the >100% case and then fails to defend against it — only `expected_total` is clamped, the already-computed per-member splits go unchanged. Treat as evidence the defensive-comment culture is itself unreliable; comments and code drift independently. | | Storage | B- | `routes/storage/uploads.rs` (C+), `routes/storage/images.rs` (C+), `routes/storage/versions.rs` (C+), `routes/storage/media.rs` (B-), `db/mod.rs::check_sandbox_cap` (C+) | `stream_url` (`downloads.rs:119-122`) computes presigned expiry as `((duration as u64) * 2).max(3600)` where `duration: i32` and no DB CHECK ≥ 0 exists on `duration_seconds`. A negative value becomes near-`u64::MAX` expiry — a centuries-long presigned URL. The cast width and missing CHECK are independent latent bugs that compose into a multi-decade credential leak. | | UX Wiring | B | `routes/pages/dashboard/wizards/item/save.rs` (B-), `error.rs` (B-), `routes/pages/public/discover.rs` (B) | `update_item` takes ~13 positional `Option`s; call sites are unreadable and error-prone. The negative-price bug (HIGH #13) is born from this signature: anyone calling it has no compiler help distinguishing `Some(-500)` (bug) from `Some(500)` (intent). | | Security | A- | `helpers.rs` (B+), `scanning/clamav.rs` (B), `scanning/yara.rs` (B), `rate_limit.rs` (B+) | The "11 layer" scan pipeline test gives a false sense of coverage. ClamAV is `FailOpen` by explicit policy (`scanning/clamav.rs:19`), YARA silently skips rule files that fail to compile (`scanning/yara.rs:54-67`), and there is no startup assertion that any real AV layer is live. A misconfigured deploy can pass EICAR as Clean while the test suite is green. | | Performance | B- | `routes/stripe/checkout/cart.rs` (C), `scheduler/announcements.rs` (C+), `scheduler/integrity.rs` (C+), `scheduler/cleanup.rs` (B-), `build_runner.rs` (B-), `db/page_views.rs` (C+), `db/pending_s3_deletions.rs` (B) | The biggest scaling cliff is a 1-line `tokio::spawn` on the page-view path, not anything that "looks expensive". Hot-path response shipped its tail-latency problem to the same pool that serves it. | ## Bug Counts by Severity | Severity | Payments | Storage | UX | Security | Perf | Total | |---|---|---|---|---|---|---| | CRITICAL | — | 1 | 1 | — | 1 | **3** | | HIGH/SERIOUS | 5 | 3 | 3 | — | 3 | **14** | | MED | 2 | 3 | 2 | 4 | 2 | 13 | | MINOR/LOW | 2 | 2 | 2 | 3 | 1 | 10 | ## Cross-Cutting Concerns 1. **Side-effects-before-validation pattern.** Storage (uploads/versions/images route gates run after scan enqueue), Payments (tip `project_id` accepted before authorization, cart `listed` not checked before checkout), UX (price `from_db` after a guard that skips zero/negative). Four files, three axes, same shape: persist first, validate later. 2. **Invariant-in-prose, fourth consecutive run.** Run #2→#3 was MaybeUser; Run #3→#4 was scan_status ordering comments-vs-code; Run #4 partial fix landed (`images.rs`) but the same disease moved up a layer to `uploads.rs` (the route-level file-type gate now runs after scan enqueue). The Payments "defensive clamp" comment in `compute_splits` is the same shape on a different organ. **No type-level constructive impossibility has yet been applied to any of these.** 3. **Optional positional args as bug carriers.** `update_item`'s ~13 positional `Option`s let the wizard pass a negative-price `Option` past the validator. Same pattern is implicated in the UX field-error finding — `ErrorTemplate`'s struct literal is missing a `fields:` field at every callsite and the compiler doesn't care. 4. **Hot-path pool pressure from fire-and-forget writes.** `record_view` per pageview, `tokio::spawn` per cart line, scheduler advisory-lock conn pinned across S3. The 25-connection pool is sized for a quiet box; three independent fan-out patterns can each saturate it. 5. **FailOpen with no liveness assertion.** ClamAV FailOpen + YARA optional + no startup gate = a green test suite can coexist with zero real AV coverage. Same shape as the Performance "spawned task accumulates without bound" pattern — both are silent degradations the operator never sees. ## Components Successfully Stress-Tested - All Run #4 Phase 1 closures verified standing (CSRF creator-tier token, `images.rs` scan_status ordering structural fix, git-shell validation, lockout `=` predicate, promo dedupe, scanner streaming + pool permit, broadcast bounded fan-out, scan_jobs retention). - Stripe HMAC: multi-secret `v1=` rotation now accepts on any match (Run #4 polish landed). - Promo `try_increment_use_count` race-free via atomic single-row UPDATE; release path uses detach for no-double-decrement; proptest-covered. - License keys: 66-bit entropy, DB UNIQUE, `FOR UPDATE` activation, full recount on revoke (display lag only — finding #M). - CSRF posture: `CsrfRouter` newtype prevents a bare `Router::route(path, post(...))` from compiling in mutation-bearing files. Verified. - Argon2id parameters + `DUMMY_HASH` timing equalization on user-not-found (login, OAuth, SyncKit). - PKCE-S256 pinned at both authorize and token endpoints; OAuth code atomic single-use consume. - JWT future-iat rejection + `jwt_invalidated_at` second-equal `<=` semantics; password change bumps `jwt_invalidated_at` via `update_user_password`. - SSE shard-guard drop-before-remove; cross-process advisory locks for scheduler ticks. - ZIP bomb: decompressed-bytes counted (not claimed); ratio + depth caps; nested magic-byte detection. - `try_increment_storage` cap-predicate UPDATE; concurrent uploads cannot both squeeze past cap. ## Confidence Per Axis - Payments **HIGH** — read 22 of 23 listed files end-to-end with targeted attacks per surface; all four SERIOUS reproducible by line-tracing. - Storage **HIGH** — CRITICAL and all three HIGHs mechanically reproducible; mandatory surprise composes two latent bugs via line-by-line read. - UX Wiring **HIGH** — full read of `csrf.rs`, `error.rs`, `markdown.rs`, `formatting.rs`, `validation/mod.rs`; spot-checked 20+ templates for CSRF pattern; CRITICAL field-aware-validation finding cross-checked by grepping `validation_fields_ref` callers. - Security **MEDIUM** — auth/CSRF/OAuth/scanning surfaces walked thoroughly; admin/moderation/reports/ssh_keys API/totp routes only sampled. ClamAV FailOpen is **policy** not bug; flagged as architectural risk. - Performance **MEDIUM-HIGH** — spot-checked DB call patterns across 15+ files; exhaustive route-level N+1 sweep deferred; stripe/webhook code shows similar `for x in &xs` loops at `checkout.rs:149,167,198,452` that were not deep-audited. ## Metrics - Modules audited: ~80 - Cold spots (≤ B): 18 - Bugs: 3 CRITICAL, 14 HIGH/SERIOUS, 13 MED, 10 MINOR/LOW - Axes at A- or above: 1/5 (Security) ## Delta Since Run #4 **FIXED (Run #4 items not surfaced this run):** - All 10 Run #4 Phase 1 items verified closed (CSRF creator-tier, `images.rs` ordering, git-shell validation, lockout email flood, cancel_pending CSRF, promo dedupe, scanner streaming + pool permit, scan_jobs retention, broadcast bounding). - All 7 Run #4 Phase 2 items verified closed (cart template price math, media reupload race, pending_uploads reaper bump, TOTP step-replay, delete_other_sessions cache eviction, `/login` CSRF, OAuth fetch_optional). - All 5 Run #4 Phase 3 items verified closed (claim_pending_build partial index, build status reaper race, `extract_s3_key_from_url` host pinning, TOTP `pending_2fa` tracking row, KNOWN_SYNC_APPS removed entirely). - All Phase 4 polish items verified closed. **NEW CRITICAL/HIGH in Run #5 (previously unaudited or regressed):** - Storage: `uploads.rs` route-level file-type gate runs after scan enqueue (CRIT). - UX: `validation_fields` plumbing is dead code at template boundary (CRIT). - Perf: `build_runner.rs` partial-failure denominator nonsense (CRIT). - Payments: NULL `item_id` decode bomb on project-level refunds (SERIOUS). - Payments: `compute_splits` over-credits when project_members sum >100% (SERIOUS). - Payments: tip `project_id` not validated vs recipient (SERIOUS). - Payments: cart bypasses item `listed` gate (SERIOUS). - Payments: unknown subscription status retry storm (SERIOUS). - Storage: `version_confirm_upload` scan enqueue before idempotency check (HIGH). - Storage: `project_image_confirm` mis-accounts on S3 probe failure + no rollback (HIGH). - Storage: `media_confirm` non-atomic three-write sequence (HIGH). - UX: negative/NaN price acceptance via `PriceCents::from_db` after permissive guard (HIGH). - UX: username availability check fails open on DB error (HIGH). - Perf: cart checkout 80 sequential roundtrips (HIGH). - Perf: `record_view` unbounded spawn per public request (HIGH). - Perf: `check_sales_count_drift` full-table aggregate (HIGH). **CHRONIC (across Run #3 → Run #4 → Run #5):** - **Invariant-in-prose / policy-not-in-types — FOURTH consecutive run.** Run #4 partially fixed the scan_status ordering inside `images.rs` (and the CSRF policy via `CsrfRouter` structurally), but the same disease *moved up a layer*: in `uploads.rs` the route-level file-type gate now runs *after* scan enqueue. The constructive-impossibility shape needed: extract a `commit_upload(file_type, ...)` higher-level operation that validates the file_type before doing any scan/credit side effects, then make `enqueue_scan_for` + `update_*_scan_status` `pub(crate)` so handlers cannot call them directly. The Payments `compute_splits` "Defensive clamp" comment + the UX `validation_fields_ref` orphan plumbing are the same disease in different organs. **REGRESSED:** - Payments (A- → B) — four new SERIOUS bugs surfaced in previously-unaudited tip/cart/refund/subscription-status corners. Not a regression in fixed code; a regression in audit coverage. - Storage (A- → B-) — invariant-in-prose recurrence (chronic above). - Performance (B → B-) — hot-path request loops audited for the first time. --- # Plan: Restore Every Axis to A- or Higher (Run #5) **Target grades:** Payments A · Storage A · UX A- · Security A- · Performance A-. User priority for the launch window: **resolve every CRITICAL/SERIOUS/HIGH before re-running**. Iterate until audits surface only small new errors. ## Phase 1 — CRITICAL (fix today) 1. **Storage CRIT — `uploads.rs` file-type gate ordering.** `routes/storage/uploads.rs:204-237`. Move the match arm that rejects `Download`/`Insertion`/`MediaImage`/`MediaVideo` BEFORE `enqueue_scan_for` and `update_item_scan_status`. Then make `enqueue_scan_for` + `update_*_scan_status` `pub(crate)` and expose a `commit_upload(file_type, item_id, s3_key)` higher-level op that performs validation → credit → row insert → status flip in the correct order. The same constructor must serve `versions.rs` and `images.rs`. This closes the chronic invariant-in-prose finding. 2. **UX CRIT — Field-aware validation reaches the UI.** `error.rs:216-264` + `templates/error.html` + `templates/partials/form_errors.html` (new). Either (a) add `fields: Vec<(String, String)>` to `ErrorTemplate` and a `{% for f in fields %}` block in `error.html` + per-input markup; or (b) delete `validation_fields*` API entirely and replace handler callsites with `validation(summary)`. Choose (a) for non-HTMX forms that need to preserve user input; choose (b) only if every existing callsite is HTMX-only and uses OOB swaps for inline errors. Audit all `validation_fields` callers and pick a path. 3. **Perf CRIT — `build_runner.rs` partial-failure denominator.** `build_runner.rs:175-180`. Track `failed_count` alongside `artifact_keys`; report `succeeded/(succeeded+failed)`. Add a test that runs 3 targets with 2 failures and asserts "1/3" in the error string. ## Phase 2 — SERIOUS / HIGH (fix this weekend) 4. **Payments SERIOUS — NULL item_id refund decode.** `db/transactions.rs:699-716`. Change return to `Vec<(TransactionId, Option)>`; `refund_transaction_by_payment_intent` caller skips `decrement_sales_count`/`revoke_keys_by_transaction` when `item_id is None`. Add a fixture-based test against a project-level transaction. 5. **Payments SERIOUS — `compute_splits` over-credit.** `routes/stripe/webhook/checkout_helpers.rs:240-269`. Reject `total_split_pct > 100` at the project_members write site (DB CHECK or validation). Defensively, scale each split proportionally when sum > 100, OR clamp each split against remaining `expected_total` budget in the loop. Add a test at 60%+60%. 6. **Payments SERIOUS — Tip project authorization.** `routes/stripe/checkout/tips.rs:104-106`. After accepting `TipForm`, fetch the project and assert `project.user_id == recipient_id`; return 400 otherwise. 7. **Payments SERIOUS — Cart bypasses `listed` gate.** `db/cart.rs:94-123` and `get_cart_items`/`get_cart_items_for_seller`. Add `AND i.listed = true` to all three queries. Add a check in the per-seller checkout path. Add a regression test that toggles an unlisted item into the cart and asserts rejection. 8. **Payments SERIOUS — Unknown subscription status.** `routes/stripe/webhook/subscriptions.rs:117-121`. Replace `?` with a match: known statuses dispatch; unknown statuses `tracing::warn!` and return `StatusCode::OK` so Stripe stops retrying. 9. **Payments SERIOUS — `is_full_refund` zero-amount.** `payments/webhooks.rs:294-308`. Predicate becomes `amount > 0 && amount_refunded >= amount`. Update the test at line 517-525 to invert (zero-amount must NOT be treated as full refund). 10. **Storage HIGH — `versions.rs` enqueue-before-idempotency.** `routes/storage/versions.rs:159-174`. Move idempotency `version.s3_key == req.s3_key` check BEFORE `enqueue_scan_for`. Apply the Phase 1 `commit_upload` helper here. 11. **Storage HIGH — `project_image_confirm` probe-failure + no rollback.** `routes/storage/images.rs:179-208`. (a) On `Err` or `Ok(None)` from `s3.object_size`, fall back to the row's recorded size (add a `project_image_bytes` column if not present) rather than the "no old image" branch. (b) Move `enqueue_deletions` to AFTER `update_project_image_url` success, or wrap both in a tx with the enqueue inside. 12. **Storage HIGH — `media_confirm` non-atomic three-write.** `routes/storage/media.rs:236-293`. Wrap `try_increment_storage` → `remove_pending_upload` → `media_files::create` in a transaction. The storage credit refund must fire on any failure path. 13. **UX HIGH — Negative/NaN prices via `from_db`.** `routes/pages/dashboard/wizards/item/save.rs:183-185, 214-227`. Use `PriceCents::new(price_cents)?` unconditionally; drop the `> 0` guard. Add `min <= suggested` check on PWYW. 14. **UX HIGH — f64 price parsing accepts NaN.** Same file + `routes/api/items/bulk.rs:136-139` + `routes/pages/dashboard/wizards/project.rs:264-298`. Parse as decimal cents directly (or `Decimal::from_str_exact` from the `rust_decimal` crate already in `Cargo.lock`); reject NaN/Inf; reject negative/saturating values before cast. 15. **UX HIGH — Username live-check fails open.** `routes/auth.rs:356-361`. Propagate the DB error or treat it as "unavailable, try again" — never "available" by default. 16. **Perf HIGH — Cart checkout sequential roundtrips.** `routes/stripe/checkout/cart.rs:68-248`. Bulk-load `has_purchased_item` once with `WHERE item_id = ANY($1)`. Batch `get_item_by_id` lookups. Claim free items in a single transaction with batched inserts. Aim for ≤ 5 roundtrips for any cart size. 17. **Perf HIGH — `record_view` unbounded spawn.** `db/page_views.rs:18-32`. Replace per-request spawn with an `mpsc` channel; one background task drains every 250ms and flushes one bulk `INSERT … ON CONFLICT … DO UPDATE SET view_count = page_view_daily.view_count + EXCLUDED.view_count`. 18. **Perf HIGH — Sales drift full-table aggregate.** `scheduler/integrity.rs:53-73`. Maintain trigger-updated `transactions_completed_count` per item, or run the check off-pool against a snapshot. Short term: add `WHERE i.sales_count > 0 OR EXISTS (SELECT 1 FROM transactions WHERE item_id = i.id LIMIT 1)` to drop the LEFT JOIN's all-zero rows from the aggregate. ## Phase 3 — MED (fix before re-run if cheap) - Storage: advisory-lock leak in `check_sandbox_cap` (`db/mod.rs:92-128`) → `pg_advisory_xact_lock` or RAII guard. - Storage: `is_s3_key_live` missing tables (`db/pending_s3_deletions.rs:67-82`) → audit all s3_key-bearing columns; consider normalized `s3_objects` table. - Storage: `delete_version` owner SELECT outside tx + post-commit S3 enqueue (`db/versions.rs:267-315`) → owner SELECT inside tx; enqueue inside tx. - Security: ClamAV `FailOpen` startup assertion (`scanning/clamav.rs:19` + `scanning/mod.rs:151-164`) → refuse boot if scan configured but no AV layer live; emit `tracing::error!` after N consecutive ClamAV errors. - Security: `helpers.rs:44-50` `DefaultHasher` for advisory lock keys → stable hasher (`sha2` first 8 bytes, or `xxh3` with constant seed). - Security: OAuth `state` size cap (`routes/oauth.rs:379-386`) → reject `form.state.len() > 1024`; cap `code_challenge` at 44 base64url chars. - Security: `extract_client_ip` non-Cloudflare fallback warning (`helpers.rs:33-40`) → emit one-shot `tracing::warn!` at startup if no `CF-Connecting-IP` seen after N requests. - UX: pagination offset overflow (`routes/pages/public/discover.rs:85-87`, `routes/admin/users.rs:37-39`) → clamp `page` to `total_pages.max(1)` before arithmetic. - UX: forms render without `_csrf` when handler forgets to populate `csrf_token` → make `csrf_token` non-optional in form-bearing templates (compile-time error) or render an inline "refresh and try again" notice. - UX: `validate_username` byte-length check (`routes/auth.rs:322`) → `chars().count()`, or reorder ASCII filter before length. - Perf: scheduler advisory-lock connection pinned across S3 (`scheduler/mod.rs:92-279`) → dedicated `PgPoolOptions::new().max_connections(1)` outside the main pool. - Perf: cleanup S3 deletes serialized inside scheduler tick (`scheduler/cleanup.rs:77-100`) → `for_each_concurrent(8, ...)`; better, move user-deletion off the scheduler tick. ## Phase 4 — Polish (after re-run shows axes ≥ A-) - Payments: `has_active_subscription_to_item` period-end clause mirroring (`db/subscriptions.rs:464-470`). - Payments: `get_active_creator_tier` + `sync_user_creator_tier` period-end defense (`db/creator_tiers.rs:91-103, 181-194`). - Payments: `release_use_count` race messaging (`db/promo_codes.rs:184-200`). - Payments: License key `activation_count` recount on revoke (`db/license_keys.rs:343-382`). - Payments: Subscription minimum-charge check (`payments/checkout.rs:283-317`). - Payments: Webhook v1/v2 unmark-on-failure parity (`routes/stripe/webhook/mod.rs:48-86`). - Storage: `media_files.list_folders` scan filter (`db/media_files.rs:73-82`). - Storage: `pending_uploads.record_pending_upload` silent user-mismatch (`db/pending_uploads.rs:23-33`). - Storage: `append_log_bounded` non-atomic size cap (`build_runner.rs:516-534`). - Storage: `downloads.rs:119-122` presigned-URL expiry: cap `duration_seconds` at i64 + add DB CHECK ≥ 0. - Security: `validate_token_consuming` for OAuth POST (`routes/oauth.rs:206`). - Security: `parse_repo_path` rejects lone-dot entries (`git_ssh.rs:162`). - Security: ClamAV INSTREAM 16K cap → treat truncation as fail-closed (`scanning/clamav.rs:101-108`). - UX: validation error messages stop reflecting user input (`wizards/item/mod.rs:176-179`). - UX: CSRF body extraction stops using `from_utf8_lossy` (`csrf.rs:528-543`). - Perf: scan-pipeline 400 MiB worst-case capacity-plan note (`constants.rs:156-157`). - Perf: announcement fan-out persistence + resume (`scheduler/announcements.rs:59-89, 147-177`). - Perf: build log per-line DB roundtrip (`build_runner.rs:516-534`) → in-process running total. ## Phase 5 — Chronic (must land in Run #6 or this audit cycle has failed) **Invariant-in-prose / policy-not-in-types, fourth consecutive run.** The Phase 1 #1 fix (constructive `commit_upload` helper sealing the lower-level ops) is the only acceptable resolution. Memory notes, comments warning future authors, and renamed-helper approaches have been tried in three prior runs and recurred each time. After Phase 1 lands, audit `compute_splits` and `ErrorTemplate` for the same shape and apply the same treatment. --- ## Headline | Axis | Run #3 | Run #4 | Direction | |------|--------|--------|-----------| | Payments | A- | **A-** | flat (1 new SERIOUS: promo over-release on cart cleanup) | | Storage | B+ | **A-** | ↑ (Run #3 image-confirm rollback/race-guard fixes verified; one residual CRIT in same file) | | UX Wiring | B+ | **C+** | ↓ (CSRF policy patchwork: missing tokens + undocumented mutation in exempt prefix) | | Security | B+ | **B+** | flat (different HIGHs: git-shell repo-name validation + lockout DoS) | | Performance | B- | **B** | ↑ (Run #3 sync-FS-in-async + DashMap shard-lock + monitor split all verified; new unbounded scan_jobs/broadcast/pool-permit findings) | Net: 4 CRITICALs (vs Run #3: 2), 10 HIGH/SERIOUS (vs Run #3: 10), 22 MED, 23 MINOR/LOW. Ship-blockers are concentrated in two structural rots — CSRF policy and scan_jobs growth — not in net-new logic mistakes. ## Critical / High Findings (fix before launch) 1. **[UX — CRITICAL]** `templates/partials/tabs/user_creator.html:80,85` — Creator-tier subscribe forms have only `tier`+`interval` hidden inputs. `/stripe/creator-tier` is not in the CSRF exempt list (`csrf.rs:166-174`). Authenticated users hitting "Monthly" or "Annual" get 403. 2. **[Storage — CRITICAL]** `routes/storage/images.rs:457-466` — `item_image_confirm` writes `scan_status` BEFORE the row UPDATE at line 508. The Run #3 fix to `uploads.rs:249-265` and `versions.rs:159-175` added a multi-line comment in each file explicitly forbidding this ordering, but `images.rs` was not updated. A lost-race rollback now flips a Clean cover back to Pending → invisible until rescan. 3. **[Security — HIGH]** `git_ssh.rs:90-102` + `db/git_repos.rs:21-35` — On first push, `parse_repo_path` only rejects `..`/leading-slash/null bytes/empty. `validate_git_repo_name` (the strict `[A-Za-z0-9._-]` allow-list) is not called before lookup or `create_repo` INSERT, and the raw name flows into `format!("{op} '/{owner}/{repo_name}.git'")` fed to `git-shell -c`. `cmd_ssh_repo_delete` validates (line 354); the dispatch path does not. 4. **[Security — HIGH]** `db/auth.rs:30-54` — `RETURNING (failed_login_attempts >= $2) AS just_locked` fires `true` on EVERY post-threshold attempt. `routes/auth.rs:161-182` mints + emails a fresh one-time login token each time. An attacker who knows a victim's email can flood their inbox. 5. **[UX — HIGH]** `routes/stripe/checkout/item.rs:390-407` — `cancel_pending_item_checkout` deletes the `pending_item_purchases` row and releases promo `use_count` BEFORE any Stripe call. It sits in the `/stripe/checkout` CSRF-exempt prefix whose documented invariant is "no state mutation occurs until Stripe's webhook confirms payment". SameSite=Lax is the only protection. 6. **[Payments — SERIOUS]** `scheduler/cleanup.rs:223` + `db/transactions.rs:1011-1029` — `cleanup_stale_pending` returns one row per deleted tx; cart checkouts produce N rows (one per cart line) carrying the SAME `promo_code_id`. The release loop calls `release_use_count` N times for a single reservation. `GREATEST(0,…)` clamp prevents negatives but lets the code be reused beyond `max_uses` when other users have legitimately incremented. 7. **[Storage — SERIOUS]** `storage.rs:179,404,629` + `scanning/worker.rs:175` — `download_object` returns `Vec`. `MAX_MEDIA_VIDEO_SIZE = 20 GB`. A single quarantine scan of a 20 GB upload OOMs the worker; multiple concurrent scans amplify. Trait has no streaming variant. 8. **[Perf — CRITICAL]** `db/scan_jobs.rs` — No pruning of completed/failed scan_jobs rows; table grows unbounded since launch. 9. **[Perf — CRITICAL]** Scanner DB pool permit held across S3 download (`scanning/worker.rs`); under any scan backlog the pool starves request traffic. 10. **[Perf — HIGH]** `routes/api/users/broadcast.rs` — unbounded sequential loop over subscribers; one broadcast can pin a worker for minutes. ## Scorecard ### Axis Summary Grades | Axis | Overall | Cold Spots | Mandatory Surprise | |------|---------|------------|--------------------| | Payments | A- | `routes/stripe/checkout/cart.rs::process_seller_checkout` neighborhood (B+), `routes/stripe/checkout/item.rs::cancel_pending` (B+) | `process_webhook_event` in `webhook/mod.rs` is shared verbatim between live HTTP handler and the scheduler retry worker; dispatcher takes pre-parsed event so replays survive Stripe secret rotation. Comment at line 108-110 calls this out explicitly. Most mature piece of payment plumbing in the audit. | | Storage | A- | `routes/storage/images.rs` (B-) | Comments in `uploads.rs` and `versions.rs` explicitly warn against the scan_status ordering bug — and reference the audit run number that caught it — yet the third handler reintroduced the exact bug. The invariant lives in prose, not types; the fix should be a shared `commit_upload_and_flip_scan_status(...)` helper so wrong ordering is uncompilable. | | UX Wiring | C+ | `routes/stripe/*` family (C+), cart templates (B+), `formatting.rs` (B+) | The `/stripe/checkout` exempt prefix has rotted into THREE incompatible models: middleware-validated, handler-validated (tip handler — documented), and silently-unvalidated-but-mutating (`cancel_pending_item_checkout` — undocumented). | | Security | B+ | `git_ssh.rs` (B-), `db/auth.rs` (B), `db/totp.rs` (B+), `db/sessions.rs` (B+) | The TOTP `pending_2fa_*` session state writes have no tracking-row entry; `delete_all_sessions_for_user` cannot sweep a phisher mid-2FA-prompt session. Intermediate authenticated state without tracking is invisible to "log out everywhere". | | Performance | B | `db/scan_jobs.rs` (B-), `scanning/worker.rs` (B-), `routes/api/users/broadcast.rs` (C+), `scheduler/completion_effects.rs` (B), `scheduler/integrity.rs` (B+) | `KNOWN_SYNC_APPS` has only insertions, no removals — deleted sync apps leak rate-limit buckets forever. The very bucket-explosion the defense was written to prevent, just on the deletion side instead of the forgery side. | ### Module Heatmap (B or below, by axis) | Module | Axis | Grade | Reason | |--------|------|-------|--------| | `routes/storage/images.rs` | Storage | B- | CRIT #2: scan_status flip before row UPDATE; comment warning ignored | | `routes/stripe/*` family | UX | C+ | CRIT #1 (missing token) + HIGH #5 (silent mutation in exempt prefix); B- cap from CRIT, dragged to C+ by HIGH #5 | | `git_ssh.rs` | Security | B- | HIGH #3: repo name unvalidated through to `git-shell -c` | | `db/auth.rs` (login lockout) | Security | B | HIGH #4: `just_locked` fires every post-threshold attempt → email flood | | `db/scan_jobs.rs` | Perf | B- | CRIT #8: no prune; table grows unbounded | | `scanning/worker.rs` | Perf | B- | CRIT #9: DB pool permit held across S3 download; SERIOUS #7: 20 GB Vec | | `routes/api/users/broadcast.rs` | Perf | C+ | HIGH #10: unbounded sequential loop | | `scheduler/completion_effects.rs` | Perf | B | Serial fan-out | | `scheduler/integrity.rs` | Perf | B+ | Sales drift full scan | | Cart templates | UX | B+ | Business logic / price math in template; thousand-sep divergence with `format_revenue` | | `formatting.rs` | UX | B+ | `format_price` thousands-sep ≠ `format_revenue` (no separators); side-by-side dashboards render inconsistently | | `db/totp.rs` | Security | B+ | `totp_last_used_step` not cleared on disable/re-enable | | `db/sessions.rs::delete_other_sessions` | Security | B+ | No `RETURNING id`; in-memory `session_cache` not evicted | | `db/builds.rs::claim_pending_build` | Storage | B+ | NOT EXISTS race; dormant on single replica, breaks on second | | `routes/storage/media.rs` | Storage | B+ | M5: delete-then-reupload races worker (no entity recheck before S3 delete) | | `csrf.rs` | UX | A- | `/login` exempt without compensating manual `validate_token` (handler does not check `_csrf`) | ## Bug Counts by Severity | Severity | Payments | Storage | UX | Security | Perf | Total | |---|---|---|---|---|---|---| | CRITICAL | — | 1 | 1 | — | 2 | **4** | | HIGH/SERIOUS | 1 | 2 | 1 | 3 | 3 | **10** | | MED | 4 | 5 | 4 | 4 | 5 | 22 | | MINOR/LOW | 3 | 5 | 6 | 5 | 4 | 23 | ## Cross-Cutting Concerns 1. **CSRF policy has rotted into a patchwork.** Three flavors of `/stripe/*` coexist: exempt-and-middleware-validated, exempt-and-handler-validated, exempt-and-unvalidated-but-mutating. One model — preferably "always-enforced + per-route opt-out macro at handler" — would have caught CRIT #1 + HIGH #5 at review time. (Same disease as Run #3's extractor-policy gap, different organ.) 2. **Invariants live in prose, not types.** The scan_status-ordering bug (CRIT #2) was documented in comments in two sibling handlers explicitly referencing prior audit runs, yet reintroduced in a third. Same pattern as Run #3's `MaybeUser` extractor sprawl: needs a constructor / helper that makes the wrong ordering uncompilable. 3. **Unbounded background tables / sets.** `scan_jobs` (CRIT #8), `KNOWN_SYNC_APPS` (Performance mandatory surprise), `pending_2fa_*` session state (Security mandatory surprise). Three independent grow-forever surfaces; each has the same shape (insert-only, no GC). 4. **Multi-replica latent bugs.** `claim_pending_build` NOT EXISTS race, `populate_known_sync_apps` startup-only, status-notification fan-out lacks cross-task cooldown. Production is single-instance; the day a second appears, three things break in non-obvious ways simultaneously. 5. **Resource-lifecycle gaps on partial-failure paths.** Media delete-then-reupload (M5), `pending_uploads` reaper bump (S4), `pending_s3_deletions` worker without entity recheck. Worker queues lack "is this entity still gone?" predicates. ## Components Successfully Stress-Tested - Stripe HMAC verification: `subtle` constant-time, multi-secret, bidirectional clock skew, distinct error strings. - Webhook dedup + retry-queue inside completion tx (Run #3 fix verified). Shared `process_webhook_event` between live + retry paths bypasses re-verify across rotation. - `charge.refunded` over-refund detection → operator-alert path. - Refund-before-charge: `pending_refunds` `FOR UPDATE SKIP LOCKED` + escalation flag. - Promo `apply_discount` — proptest + 25 adversarial tests including negative discount, over-100% clamp, integer rounding. - License keys: 66-bit entropy + DB UNIQUE + `FOR UPDATE` activation lock + full recount. - Stripe Connect race: atomic claim + orphan-cleanup log. - 4-tier scheduler decomposition (Run #2 fix verified): advisory-locked per tier. - Monitor decomposition into 3 cadences (Run #3 fix verified). - `try_increment_storage` cap-predicate UPDATE — concurrent uploads cannot both squeeze past the cap. - `pending_s3_deletions` `FOR UPDATE SKIP LOCKED` + dead-letter (Run #3 fix verified). - `delete_objects`: chunked at 1000, JoinSet-bounded 4-way. - S3 multipart streams from disk in `build_runner` (Run #3 fix verified). - Argon2id 46MiB/2it + `DUMMY_HASH` timing equalization on user-not-found. - PKCE-S256-only at both endpoints + atomic OAuth-code consume. - JWT future-iat rejection (60s skew) + `jwt_invalidated_at` second-equal rejection (Run #3 fix verified). - Session fixation: `cycle_id` on login + `flush` on logout + fresh CSRF cycled with session ID. - ZIP bomb: decompressed-bytes (not claimed) tracked, ratio + depth caps, encoded-traversal, nested magic-byte detection beats extension renames. - YARA 30s scan timeout; ClamAV circuit breaker (10 errors → flip clean→held). - Rate-limit bucket-explosion defense (forged JWT app IDs collapse to nil). - IP extractor: `CF-Connecting-IP` only; never XFF. - CSRF middleware: `url::form_urlencoded` parsing (textarea-smuggling defense); multipart rejected with audit log; 2 MiB body cap. - `json_escape` blocks JSON-LD script-tag injection. - Slugify + CSV-injection prefix safeguards (`=`, `+`, `-`, `@`, tab, CR + ZWS variants). - `validate_link_url`: `javascript:`/`data:` rejected. - SSE shard-guard drop-before-remove pattern (Run #3 fix verified); same care now applied to push side. - Discover pagination clamp (Run #3 fix verified). - `is_localhost_redirect` via `url::Url::parse` (Run #3 fix verified). ## Confidence Per Axis - Payments **HIGH** — webhook chain read end-to-end; S1 reproducible by line-tracing. - Storage **HIGH** — CRIT #2 mechanically reproducible; mandatory surprise documents the exact prior-run audit footprint. - UX **HIGH** for CSRF findings (#1, #2 — er, #5); **MEDIUM** for markdown XSS surface (docengine sanitization not audited). - Security **MEDIUM-HIGH** — git-shell exploit needs empirical confirmation against the parser; gap is real either way. TOTP `pending_2fa_*` finding inferred from auth.rs:194-210; 2FA-finish handler not read. - Performance **MEDIUM-HIGH** — broad routes coverage; admin/items routes sampled, not exhaustive. Schema not opened; some index claims inferential. ## Metrics - Modules audited: ~75 - Cold spots (≤ B): 16 - Bugs: 4 CRITICAL, 10 HIGH/SERIOUS, 22 MED, 23 MINOR/LOW - Axes at A- or above: 2/5 (Payments, Storage) ## Delta Since Run #3 **FIXED (Run #3 items not surfaced this run):** - Run #3 CRIT `images.rs` no-rollback / no-race-guard ×2 — partial fix landed: race-guard backported, rollback for storage credit + S3 orphan landed. Residual: the scan_status ordering inside the same fix was missed (becomes Run #4 CRIT #2). - Run #3 HIGH `MaybeUserVerified` suspension not enforced — fix verified in `auth.rs` extractors; security agent did not re-flag. - Run #3 HIGH JWT iat second-resolution race — fix verified in `synckit_auth.rs` (`<=` comparison). - Run #3 HIGH sync FS / `reqwest::Client::new()` in dashboard/exports/`create_repo` — fix verified; performance agent did not re-flag. - Run #3 HIGH DashMap shard-lock across SSE push — fix verified at `routes/synckit/sync.rs:104`. - Run #3 HIGH monitor task overload — fix verified (3 cadences in `monitor.rs:112-117`). - Run #3 SERIOUS cart drift validation parity — fix verified (`validate_cart_against_live_items` extracted; both paths call it). - Run #3 SERIOUS scan_status flip ordering in `uploads.rs` + `versions.rs` — fix verified; comments documenting the fix are now part of the surface area of Run #4 CRIT #2 (the bug they warn against was reintroduced in `images.rs`). - Run #3 SERIOUS media double-decrement race — fix verified (`RETURNING *` + `is_some()` gate). **CHRONIC (across Run #3 → Run #4):** - **Invariant-in-prose / policy-not-in-types.** Run #2→#3 was MaybeUser extractor sprawl. Run #3→#4 is scan_status ordering comment-vs-code drift AND CSRF policy patchwork. Different organ; same disease. **Structurally unfixed for 3 consecutive runs.** **REGRESSED:** - UX Wiring (B+ → C+) — CRIT #1 (missing CSRF on creator-tier forms) + HIGH #5 (silent mutation in CSRF-exempt prefix) + cart template math. **NEW HIGHs/CRITs in Run #4:** - Missing CSRF token on creator-tier forms (CRIT). - `cancel_pending_item_checkout` mutation in CSRF-exempt prefix (HIGH). - `git_ssh.rs` repo-name unvalidated to `git-shell -c` (HIGH). - Login lockout `just_locked` per-attempt email/token flood (HIGH). - Promo `use_count` over-release on cart cleanup loop (SERIOUS). - Scanner `Vec` RAM blowup on 20 GB media (SERIOUS). - `scan_jobs` unbounded growth (CRIT). - Scanner DB-pool permit held across S3 download (CRIT). - `broadcast.rs` unbounded sequential loop (HIGH). --- # Plan: Restore Every Axis to A- or Higher **Target grades:** Payments A · Storage A · UX A- · Security A- · Performance A-. ## Phase 1 — Clear HIGH/CRITICAL caps (must do before launch) 1. **CSRF token on creator-tier forms (CRIT #1).** Add `{% if let Some(token) = csrf_token %}{% endif %}` to both forms in `templates/partials/tabs/user_creator.html:80,85`. Verify the handler context populates `csrf_token`. 2. **`images.rs` scan_status ordering (CRIT #2).** Move `update_item_scan_status` to AFTER the `Ok(true)` arm at `images.rs:519` — matching the documented ordering in `uploads.rs:249-265` and `versions.rs:159-175`. Land a shared helper (e.g. `commit_upload_and_flip_scan_status(...)`) so future handlers cannot get the ordering wrong. 3. **`git_ssh.rs` repo name validation (HIGH #3).** Call `validate_git_repo_name(repo_name).map_err(|_| anyhow!("repository not found"))?` immediately after `parse_repo_path` succeeds in the dispatch path. Add the same check in `db::git_repos::create_repo`. Match the discipline `cmd_ssh_repo_delete` already has at line 354. 4. **Login lockout `just_locked` (HIGH #4).** Change `db/auth.rs:30-54` `RETURNING` clause to `(failed_login_attempts = $2) AS just_locked` (exact equality on the crossing attempt). Follow-up: cap the lockout-email rate independently via an idempotency key on `email_outbox`. 5. **`cancel_pending_item_checkout` CSRF gap (HIGH #5).** Either narrow the `/stripe/checkout` exempt prefix so this path isn't in it, OR add explicit `csrf::validate_token` like `create_tip_checkout` does. Document the rule at the prefix definition site so the next "I'll just add a quick mutation handler" landing fails review. 6. **Promo `use_count` over-release on cart cleanup (SERIOUS #6).** Dedupe `promo_ids` in `cleanup_stale_pending_transactions` (`HashSet` before the release loop) OR have `cleanup_stale_pending` SQL return `DISTINCT promo_code_id`. 7. **Scanner `Vec` → stream (SERIOUS #7).** Add `download_stream`/`get_object_stream` to the S3 trait returning a `ByteStream`. Wire the scanner worker to pipe through ClamAV instead of buffering 20 GB into memory. 8. **`scan_jobs` retention (CRIT #8).** Add a scheduler tier (hourly) that deletes `scan_jobs` rows older than N days where status ∈ {Clean, Quarantined, Failed}. Keep Pending/Running. Document N (default 30 days) in `constants.rs`. 9. **Scanner DB-pool permit (CRIT #9).** Drop the pool permit before the `download_object` await; reacquire after the bytes are local. Or move the S3 fetch outside the DB-bounded code path entirely. 10. **`broadcast.rs` unbounded loop (HIGH #10).** Move broadcast send into `tokio::spawn` with a bounded `JoinSet` (cap 16) over recipient chunks; per-recipient send already has 100ms shape — keep it. ## Phase 2 — Close axis-dragging SERIOUS items 11. **Cart template price math.** Move price-formatting out of templates; either pass pre-formatted strings from the handler or expose `format_price` as an Askama filter. Unify with `format_revenue` (thousands sep). 12. **`media.rs` delete-then-reupload race (Storage M5).** Worker re-checks `SELECT 1 FROM media_files/items/versions WHERE s3_key = $1` before each S3 delete. OR queue inserts carry the entity-ID and the worker confirms absence. 13. **`pending_uploads` reaper bump (Storage S4).** ON CONFLICT only refresh `created_at` when `user_id` matches; otherwise treat as fresh row. 14. **TOTP step-replay across re-enable (Security MED).** `disable_totp` clears `totp_last_used_step` (NULL); `set_totp_secret` resets to 0 / NULL. 15. **`delete_other_sessions` cache eviction (Security MED).** Return `RETURNING id` so callers can evict `state.session_cache`. 16. **CSRF on `/login` (Security MED).** Move `/login` out of `exempt_prefixes` and have `login_handler` call `csrf::validate_token` like `authorize_post` does. The login template already renders the token. 17. **`is_registered_redirect_uri` `fetch_one`→`fetch_optional` (Security MED).** Return `Ok(false)` on `None`; document trailing-slash matching policy. ## Phase 3 — Resilience & infra hardening 18. **Multi-replica `claim_pending_build`.** Add `CREATE UNIQUE INDEX … ON ota_builds(status) WHERE status='running'` partial unique index, OR explicit `pg_advisory_xact_lock` around claim. 19. **Build status race with stale reaper (Storage M4).** Add `WHERE status='running'` to the success UPDATE and check `rows_affected`. 20. **`KNOWN_SYNC_APPS` deletion path.** Add `unregister_known_sync_app` called from sync-app delete. Long-term: switch from `OnceLock` to a DB-backed cache with TTL so multi-replica deploys don't diverge. 21. **`extract_s3_key_from_url` host pinning (Storage S3).** Require the `https://` host to be the configured S3 endpoint or a known CDN domain. 22. **TOTP `pending_2fa_*` tracking row (Security surprise).** Insert a temporary tracking row at the moment 2FA-pending begins, scoped via `kind='pending_2fa'` so `delete_all_sessions_for_user` can sweep it. ## Phase 4 — Polish 23. **`MaybeUserUnverified` rename.** Rename to make the danger boundary impossible to forget at use site (e.g. `SessionUserStaleAllowed`), or short-circuit to `None` when the session lacks `SESSION_TRACKING_KEY`. 24. **`sum_file_sizes_for_item` clamp direction (Storage M6).** `GREATEST(0, LEAST(SUM, i64::MAX))::BIGINT`. 25. **License key collision retry (Payments M3).** Retry once on 23505 in `create_license_key` and the promo-claim arm. 26. **Stripe v1-rotation header (Payments M1).** Collect all `v1=` values; accept on any match. 27. **`has_active_subscription_to_project` period-end check (Payments M2).** `AND (current_period_end IS NULL OR current_period_end > NOW())`. 28. **HTML sniff strengthening (Security MED).** For Download type, add a string sniff for `