Section 14 — Application Security
Companion to
musehub-production-readiness-checklist.md.
Update (2026-09-07) — remaining gaps reviewed with real code inspection
Every item this section previously left as "not reviewed" was actually checked against the code this pass, not just assumed.
SSRF review — resolved, and it's solid
musehub/security/ssrf.py is a dedicated, well-built module: HTTPS-only enforcement, a
comprehensive blocked-range list (RFC-1918, loopback, link-local — including the AWS metadata
endpoint 169.254.169.254, carrier-grade NAT, IPv6 equivalents), and a two-layer design —
check_url_safe() for fast sync validation at model/field-parse time, validate_outbound_url()
for full async DNS-resolution checking (blocks DNS-rebinding) immediately before delivery.
Confirmed both are actually wired in, not just defined: musehub/models/musehub.py calls
check_url_safe on webhook URL fields, musehub/services/musehub_webhook_dispatcher.py calls
validate_outbound_url before every delivery attempt. (The original doc guessed this lived in
musehub/worker.py — it doesn't; that file is the unrelated background job processor. The real
logic is in the webhook dispatcher.)
CSRF review — resolved, genuinely N/A
Grepped the entire codebase for set_cookie/Response.cookie/session-cookie patterns — zero
matches. No cookie-based sessions exist anywhere, so the classic CSRF attack vector (browsers
automatically attaching cookies to cross-site requests) doesn't apply. This isn't a "likely low
risk" hedge anymore — it's confirmed there's nothing for CSRF to exploit.
WebSocket/SSE authentication — resolved, correctly implemented
The MCP endpoint (musehub/api/routes/mcp.py) and the social-feed SSE endpoint
(musehub/api/routes/musehub/social.py) both use optional_signed_request/optional_token at
the connection level — deliberately allowing anonymous connections for read-only/discovery access
— but enforce a required fresh MSign signature deeper in the call for privileged actions
(confirmed by reading the actual dispatch logic, not just the dependency declaration). TokenClaims
and optional_token in musehub/auth/dependencies.py are just re-exported aliases for
MSignContext/optional_signed_request from the real implementation in
musehub/auth/request_signing.py — one auth system throughout, not two parallel ones.
MSign replay protection — real finding: it's freshness-only, not true replay prevention
Read musehub/auth/request_signing.py closely. The ts field check is:
if abs(server_ts - ts) > REPLAY_WINDOW_SECONDS: # default 30s
raise HTTPException(401, ...)
This bounds how long a captured signed request stays valid — it does not track whether a
given (handle, ts, sig) has already been used. Compare against the codebase's own separate
nonce mechanism (musehub/services/musehub_auth.py's auth-challenge flow, and MPay's
nonce_hex with a DB UniqueConstraint) — both of those genuinely prevent replay via single-use
enforcement. Per-request MSign signing has no equivalent. The module's own docstring calling this
"replay protection" overstates what it does; it's a freshness/skew check.
Practical severity is real but narrow: exploiting this requires an attacker to capture a validly-signed request in transit within its 30-second window (network position, compromised proxy/logging, or — relevant here, since Cloudflare terminates TLS at the edge and re-encrypts to origin — anyone with edge-layer visibility) and the replayed action needs to have a non-idempotent side effect for the replay to matter. GET requests replaying harmlessly; a captured POST/DELETE replayed within the window would re-execute.
Not fixed in this pass — the real fix (a short-lived (handle, sig) dedup cache, e.g. a
Postgres table or Redis key with TTL matching REPLAY_WINDOW_SECONDS, checked on every
authenticated request) is genuine engineering work with real performance implications (a write on
every authenticated API call, and needs to work correctly across the blue/green instance
topology). Flagging precisely rather than either overstating the current risk or quietly leaving
the docstring's inaccurate claim uncorrected.
Account/data deletion — a mechanism exists
Confirmed soft-delete via deleted_at on identities (musehub/api/routes/musehub/users.py,
consistent with the deleted_at.is_(None) filter already seen in the auth path). Not deeply
audited for completeness (e.g. whether all related data — repos, issues, comments — cascades
correctly), just confirmed the mechanism isn't entirely absent.
Still open — genuinely needs Gabriel or a dedicated pass, not code-checkable from here
- [ ] Uploaded content scanning (malware/content scanning beyond size/structure limits)
- [ ] Data classification and retention policy — organizational decision, not a code question
- [ ] Correct the misleading "replay protection" docstring language in
request_signing.py, and decide whether the dedup-cache fix is worth building before or after launch - [ ] Formal OWASP-oriented pass — this section and #161 together cover most of the Top 10 categories already; a dedicated closer-to-launch pass (Section 17) should build on these findings rather than starting cold
What's already solid — confirmed by code review (from the original pass, still true)
- No SQL injection surface (parameterized queries throughout)
- Path traversal explicitly rejected in the coordination module
- Archive/decompression bomb protection (
mpack_max_decompressed_bytes, 4 GB cap) - Debug mode off by default;
/docs//redoc//_debug/memoryall correctly gated - No passwords stored anywhere (MSign is signature-based)
- Rate limiting on auth-adjacent endpoints
- CORS fails closed
- Security headers and CSP set (
X-Frame-Options,X-Content-Type-Options, CSP, HSTS) - Thorough, deliberate upload/quota limits (size, commit count, object count, per-user/per-repo quotas, daily upload cap)