Fixed confirmed findings before public exposure:
HIGH:
- upload filename path traversal → store.safe_name() strips to basename
- login rate-limit XFF bypass → key on request.client.host + per-username bucket;
auth.check_login() burns bcrypt time on unknown users (no enumeration)
- cross-user read access → per-user isolation: guests see/use/download/delete only
their own assets & jobs (owner sees all); WS job events scoped per-user
MEDIUM:
- unbounded upload read → bounded chunked streaming to the 1GB cap
- asset member path check → Path.is_relative_to boundary + ownership gate
- WS token-in-query-string leak → session-cookie-only WS auth
LOW:
- retry_job bypassed the per-user job cap → cap now checked on retry
- wholesale API-key injection → env_for_operator injects a paid key only to
operators that declare it (guest local jobs never receive fal/OpenRouter keys)
- session revocation → users.session_epoch, bumped on password change
- int() 500s → 400; net-lane defense-in-depth (guests blocked by requires_env AND
resources==net, so a mis-tagged paid op is still blocked)
+ public /api/health for serve.sh & proxy; docs/VPS.md; mb MB_TOKEN bearer auth
tests/smoke.sh: 34 checks passing incl. all new hardening.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Operators (16 total):
- fal_hunyuan3d_v21: Hunyuan3D 2.1 single-image (live-verified dash variant,
~90s; v21 multi-view is broken on fal — v2 stays the multi-view path)
- fal_bg_remove (BiRefNet v2): subject cutout pre-pass — the biggest quality
lever before image->3D
- fal_upscale (SeedVR faithful upscale), fal_image_edit (nano-banana prompt
edits), fal_text_image (Ideogram v3 — first no-input operator)
- fal_common: collect='images' mode; recursive URL extractor now takes the
wanted extension set
UI: operator dropdown grouped by category (optgroup); operators with
accepts: [] run without a selected asset ('No input asset needed' note);
run-button/launch logic updated accordingly.
Review fixes (Opus Phase 1 commit):
- runner._run_lane crashed (TypeError) when a queued job was deleted before
the worker picked it up
- PUT /api/settings treated empty string as a masked placeholder, making
secrets impossible to clear from the UI
- store.register_file mutated the caller's meta dict via pop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>