Date: 2026-06-11
Branch reviewed: main (after PR #181 + security/auth-boundary-fixes)
Previous audits: docs/audits/rustchat-production-gap-analysis.md, docs/audits/rustchat-implementation-consistency-and-gap-analysis.md
RustChat has matured significantly since the May 2026 audits. Backend unit tests grew from 143 → 173, frontend unit tests from 108 → 300, four P0 security gaps were closed, file upload authorization was fixed, and the native file API now proxies downloads instead of leaking presigned S3 URLs.
Verdict: The codebase is approaching public preview readiness but is not yet production-grade. The remaining blockers are concentrated in:
- Realtime durability (in-memory replay, no graceful shutdown, silent event drops)
- Authorization completeness (scheduled post delete, bulk membership revocation)
- Observability & audit (audit logs only used in 2 places, readiness lacks migration check)
- Message validation & pagination (no idempotency, offset pagination on hot paths)
- Rate limiting coverage (file upload and search unprotected)
| Gap | Prior Severity | Status | Evidence |
|---|---|---|---|
| GAP-1: WS channel subscription auth | P0 | CLOSED | websocket_core.rs checks is_channel_member before subscribe |
| GAP-2: WS typing event auth | P0 | CLOSED | Both v1 and v4 typing paths validate membership |
| GAP-3: Frontend reconnect state | P0 | CLOSED | useWebSocket.ts tracks connection_id + last_seq |
| GAP-4: Transactional post core | P0 | CLOSED | create_post uses service-level tx for DB side effects |
| F-001: Native file upload membership | P0 | CLOSED | files.rs::upload_file calls check_upload_channel_access |
| F-002: Presigned URL leak | P0 | CLOSED | Upload/download return /api/v1/files/{id}/content proxy URLs |
| F-003: v4 post action stubs | P1 | CLOSED | All return 501 Not Implemented via post_action_not_implemented |
| F-004 create/update: Scheduled post auth | P1 | CLOSED | create_scheduled_post and update_scheduled_post call require_channel_membership |
| Cross-team channel/DM access | P1 | CLOSED | channels.rs now verifies requester team membership |
| Password change verification | P1 | CLOSED | change_password verifies current_password against hash |
| Agent KB team scoping | P1 | CLOSED | knowledge.rs checks agent creator is in same team as KB |
| Status endpoint auth | P2 | CLOSED | get_user_status and get_statuses_by_ids require AuthUser |
| GAP-23: SVG validation | P1 | CLOSED | SVG blocked at extension level; validate_svg is dead code |
| GAP-21: Markdown XSS | P1 | CLOSED | DOMPurify + 10-case XSS corpus test suite exists |
| GAP-25: Secrets in repo | P2 | CLOSED | .gitignore excludes .env and secrets/; nothing tracked |
- Risk: Server restart or >128 messages between reconnects = lost events
- Evidence:
connection_store.rsusesVecDequebounded to 128, TTL 5 min. No outbox / durable event table. - Fix: Document replay as best-effort + require REST reconciliation on reconnect, or add durable outbox.
- Risk: Users removed from channels may continue receiving events
- Evidence:
remove_channel_member_by_id(v4) andremove_member(v1) now unsubscribe, BUT:admin_teams.rs::remove_team_memberdoes not unsubscribev4/teams/members.rs::remove_team_memberdoes not unsubscribeleave_teamunsubscribes from team but not from individual channels
- Fix: Add hub unsubscribe to all membership removal paths.
- Risk: Users display stale state without knowing they missed events
- Evidence:
websocket_actor.rsdrops events when queue full (256).connection.rsdrops on broadcast lag. Noresync_requiredevent or close code sent. - Fix: Send
resync_requiredevent or close with policy code on overflow/lag.
- Risk: Deploys drop all WS connections abruptly
- Evidence:
main.rsuses bareaxum::serve(...).awaitwith nowith_graceful_shutdown, no SIGTERM/SIGINT handler.SERVICE_RESTARTclose code (1012) is defined but never used. - Fix: Add signal handling, stop accepting new connections, drain WS with 1012 close code.
- Risk: Users can delete scheduled posts for channels they left
- Evidence:
v4/posts.rs:1151-1177checks ownership viadelete_scheduled_post(scheduled_id, auth.user_id)but never verifies current channel membership. - Fix: Fetch scheduled post's
channel_id, thenrequire_channel_membershipbefore delete.
- Risk: Race condition between pre-tx read and tx write
- Evidence:
services/posts.rs:352-387validates file ownership/channel before tx.services/posts.rs:407-423links files inside tx withUPDATE ... WHERE post_id IS NULL, but does NOT re-verifyuploader_idorchannel_id. - Fix: Move ownership/channel validation inside the transaction.
- Risk: K8s routes traffic to nodes with pending migrations
- Evidence:
api/health.rs::readinesschecks DB, Redis, S3, email outbox, but NOTschema_migrations/ pending migrations. - Fix: Query migration state in readiness probe.
- Risk: Security incidents are un-investigable
- Evidence:
insert_admin_audit_logexists but is only called in 2 places (admin_users.rs:251soft-delete,admin_users.rs:314wipe). Missing for: permission changes, config changes, team/channel deletion, SSO/email changes, file access, failed logins. - Fix: Add audit calls to all security-sensitive admin/auth actions.
- Risk: Oversized messages, malicious markdown, duplicate messages
- Evidence:
validate_create_postchecks empty, max length, max files, root post validity, but NOT:- Markdown/HTML safety (backend trusts frontend sanitizer)
client_msg_iddeduplication (accepted in API but never checked for duplicates)
- Fix: Add idempotency check on
client_msg_id; consider backend markdown safety validation.
- Risk: False positive mentions in code blocks, URLs, inline code
- Evidence:
services/unreads.rsuses PostgreSQL regexLOWER(message) ~ patterndirectly on raw message text.parse_mentionsinposts.rs(code-block/URL-aware) is never called by unreads. - Fix: Either pre-compute mentions at post creation and store in a
post_mentionstable, or make unreads use the same parser as post creation.
- Risk: Missing/duplicate messages during concurrent inserts
- Evidence:
PostRepository::list_by_channelusesOFFSETordering bycreated_at DESC. Keyset methods exist but are not the default. - Fix: Migrate channel history to cursor pagination.
- Risk: Resource exhaustion, enumeration
- Evidence: Auth (login/register/reset) and WS connect have IP rate limits. File upload (
v4/files.rs,v4/uploads.rs) and ALL search endpoints (v4/posts/search,v4/users/search,v4/files/search,api/search.rs) have no rate-limit middleware. - Fix: Add per-IP and per-user rate limits to upload and search.
| Gap | Risk | Current State |
|---|---|---|
| GAP-17: Migration verification insufficient | Bad migration blocks/corrupts production upgrades | No CI job tests empty→latest or snapshot→latest migrations |
| GAP-18: Error contract inconsistent | Clients cannot reliably handle errors | Error mapping exists but negative contract tests are sparse |
| GAP-19: Undocumented v4 stubs | Clients depend on incomplete surfaces | Some stubs now return 501, but full endpoint inventory missing |
| GAP-20: Optimistic message retry incomplete | Users see false sent states or duplicates | client_msg_id exists but no dedup on backend |
| GAP-22: Frontend/backend permission parity | UI hides actions backend still allows | Permission matrix exists but no end-to-end parity tests |
| GAP-29: Fanout needs scale limits | Large channels cause CPU/memory spikes | WsHub::broadcast_local is linear; no load tests |
| GAP-30: Large channel frontend rendering | Scroll jank, memory pressure | No virtualization observed; no perf E2E for 5k messages |
- 63
unwrap()/expect()calls insrc/api/,src/services/,src/repositories/(excluding tests). Most are in v4 websocket connection mapping functions where data shapes are controlled, but a few are in API handlers (e.g.,channels/posts.rs:first().unwrap(),turn.rs:88 unwrap()). - No
panic!()calls in production code paths. - All SQL queries are parameterized (
bind()usage verified). No raw string interpolation of user input observed. - cargo clippy: passes cleanly.
- cargo audit: 0 vulnerabilities.
- 230 ESLint warnings (up from 200 in prior audit). Mostly
anytypes. No errors. - 7
v-htmlusages in 5 files. All pipe through DOMPurify + corpus tests. - npm audit: 0 vulnerabilities.
- Build: passes.
Before calling RustChat "production ready":
| Gate | Current | Target |
|---|---|---|
| Backend unit tests | 173 passing | 250+ (permission matrix, message validation, idempotency) |
| Frontend unit tests | 300 passing | Maintain + add markdown/reconnect corpus |
| Integration tests | Partial (needs live DB) | Full suite in CI with Postgres/Redis/S3 |
| Contract tests | Partial v4 schemas | All public v4 endpoints + error shapes |
| E2E tests | 5 specs | Full auth→send→edit→delete→upload→search→unread flow |
| cargo clippy | Passes | Enforce -D warnings |
| cargo audit | 0 vulns | Maintain weekly |
| npm audit | 0 vulns | Maintain weekly |
| Frontend lint | 230 warnings | Ratchet to 100, then 0 |
| Migration test | None | CI job: empty→latest + snapshot→latest |
| Load test | None | WS fanout baseline (1k/5k conns) |
| Secret scan | GitGuardian configured | Add pre-commit hook |
- Add
delete_scheduled_postchannel membership check — 1 file, ~5 lines - Add rate limiting to file upload and search endpoints — middleware layer, ~3 routes
- Add migration check to readiness probe —
health.rs, 1 query - Move file ownership validation inside post creation transaction —
services/posts.rs - Add graceful shutdown with WS drain —
main.rs+ signal handling - Add slow-client resync signal —
websocket_actor.rs/connection.rs - Expand audit logging to admin/auth actions — instrument 10+ handlers
- Add
client_msg_ididempotency check —services/posts.rs - Fix remaining subscription revocation gaps —
admin_teams.rs,v4/teams/members.rs,leave_team - Add integration tests for the above —
backend/tests/
Current maturity: Internal beta / early public preview
RustChat is solid enough for trusted users and active engineering support. The remaining P0/P1 gaps are operational and authorization completeness issues, not fundamental architectural problems. The recent fixes (transactional posts, WS auth, file upload auth, reconnect state) show the codebase can be hardened systematically.
Production readiness ETA: After closing the 10 actions above plus adding integration tests and a migration CI gate, RustChat would be suitable for controlled production (small-to-medium teams, with active monitoring). Full enterprise production would additionally require durable event replay, cursor pagination, and load-tested fanout limits.