Fix bug-audit round 3: PG image decode, theme SSR, SSE resilience, async I/O

Fix bug-audit round 3: PG image decode, theme SSR, SSE resilience, async I/O

#49 in Riparion/riparion-cms — merged 2026-06-02

Summary

A multi-agent audit (DB/SQL, auth/authz, XSS/escaping, Dioxus/SSR, logic & KISS/DRY) of the codebase. Each finding was verified against the actual code to rule out false positives before fixing. The codebase came out well-defended — no SQL injection, no auth-bypass, no stored-XSS were found. This PR addresses the real defects and worthwhile cleanups.

HIGH

  • Postgres responsive images broken. srcsets_for_urls decoded media_variants.width (INTEGER/int4) into i64. sqlx-postgres is strict about integer width, so this errored at runtime for any post with local-upload renditions — the whole responsive-image feature failed on the Postgres backend (worked on SQLite, which decodes all ints as i64). Now decoded as i32, matching the insert side.
  • Theme hue FOUC / missing SSR styling. The --brand-hue override <style> was fetched with use_resource, which doesn't resolve during SSR — custom-hue sites rendered the compiled-in default server-side and snapped after hydration. Now resolved server-side via a new ThemeStyle component (use_server_future), so the override is baked into the SSR HTML (same fix already applied to feed_layout).

MEDIUM

  • SSE loops survive error frames. use_live / use_admin_live used while let Some(Ok(..)), tearing the EventSource down permanently on the first Err (e.g. the admin stream's 403, or any terminal network error), killing live presence/comments/reactions until a full reload. They now skip Err frames instead.
  • Async disk I/O + shared rendition helper (DRY). Extracted persist_renditions (uses tokio::fs, logs failures) shared by the upload path and the backfill — previously duplicated and drifting. Media upload/delete now use tokio::fs so disk writes no longer block a tokio worker, and variant-insert failures are logged rather than silently dropped (which orphaned renditions on disk).
  • Bounded slug allocation. unique_slug capped its sequential -2, -3, … probing, then falls back to a random hex suffix.

LOW

  • escape_attr the srcset/sizes values in rewrite_inline_images (parity with the ResponsiveImg widget).
  • Generous length caps on authored post/page title/body/excerpt/seo_description (parity with the comment path).
  • RelatedPosts reads its resource via read() (subscribed) into an owned value instead of read_unchecked().

Deferred items (intentional trade-offs / larger changes) are documented in the local TODO_BUGS.md.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --no-default-features --features server,sqlite --all-targets -- -D warnings
  • cargo clippy --no-default-features --features server,postgres --all-targets -- -D warnings
  • cargo clippy --no-default-features --features web --target wasm32-unknown-unknown -- -D warnings -A dead_code
  • cargo test --no-default-features --features server,sqlite → 42 passed ✅
  • Postgres test suite runs in CI (needs a live DB).

🤖 Generated with Claude Code

Last updated 2026-06-03