Multiverse Campus β Consolidated Audit Report
Auditor: Nexus / Liberation Labs
Date: 2026-07-10
Target: multiversecampus (GitHub: lizTheDeveloper/multiversecampus)
Scope: Source code review only β no live system testing
Method: 3 rounds automated scan + manual review, adversarial red team on all findings
Tools: Trivy (dep vulns), Semgrep (7 custom + 3 public rulesets), 6 dedicated review agents
Executive Summary
The Multiverse Campus codebase is well-defended against injection attacks β zero SQL injection across 396 files with SQL, and proper XSS escaping in flagged endpoints. Authentication middleware is correctly applied across all 75 route files with only one exception.
However, the review found 2 critical, 4 high, 5 medium, and 2 low confirmed issues across security and functionality. The most impactful: lectures cannot be started in production (type mismatch), and a webhook endpoint silently disables signature verification when an env var is missing.
30 findings were submitted to adversarial red team. 4 were false positives (red team caught that we missed auth middleware at the route mount point in one case). 11 were overstated (severity reduced or reclassified). 13 survived as confirmed issues and 2 additional false positives were identified in our own initial report, which are documented below for transparency.
Confirmed Findings
CRITICAL
C1: Lecture instructor_id type mismatch β all lectures brokenserver/src/services/lectureCapture.ts:48,367 + server/src/services/socketHandlers/lectureHandlers.ts:97,125,153
socket.userId is a numeric string ("42") but lecture_sessions.instructor_id is a UUID column. PostgreSQL rejects the insert, so startLectureSession always throws. Even if data were inserted, isSessionInstructor uses strict equality (===) between a numeric string and a UUID string β always false. No instructor can start, pause, resume, or end their own lecture.
Impact: Lecture system is non-functional in production.
Fix: Use consistent ID types β either convert socket.userId to UUID format or change the column type.
C2: School webhook signature bypass when env var is unsetserver/src/routes/webhook.ts:219-220
const secret = process.env.SCHOOL_WEBHOOK_SECRET || ''
if (!secret) return true // No secret configured = skip verification (dev mode)
When SCHOOL_WEBHOOK_SECRET is not set, all signature verification is skipped. The endpoint (mounted without auth middleware) accepts arbitrary POSTs that can mark exercises complete, inject submissions, or delete exercise awareness rows for any student email. No NODE_ENV guard.
Impact: Unauthenticated exercise data injection in production if env var is missing.
Fix: Reject all requests when secret is unset, or require NODE_ENV=development for the bypass.
HIGH
H1: JWT_SECRET falls back to empty string β server starts without a signing secretserver/src/middleware/auth.ts:12-13
getJwtSecret() returns process.env.JWT_SECRET ?? ''. If unset, all JWTs are signed and verified with an empty string. The startup warning logs an error but does not halt the process. Combined with local dev tunneling to prod Postgres, a developer running without JWT_SECRET can forge tokens against real user data.
Fix: process.exit(1) if JWT_SECRET is empty or matches a placeholder.
H2: connect_error triggers rapid reconnection loopclient/src/stores/presenceStore.ts:578-598
On any connect_error, the handler calls refreshToken() then connect() with no backoff and no check for error type. If the server is down but the auth endpoint is reachable, this creates a tight loop of thousands of reconnection attempts per second, pinning CPU and draining battery on mobile.
Fix: Add exponential backoff. Only refresh token on 401/403 errors, not all connection failures.
H3: Gem debit without transaction protection on housing purchaseserver/src/routes/store.ts:1108-1129
debitGems() and purchaseAdditionalProperty() are separate operations not wrapped in a single transaction. If property creation fails after the debit succeeds (constraint violation, DB hiccup), gems are lost. Same pattern at line 1153 for hall purchases.
Fix: Wrap both operations in a single DB transaction.
H4: Trade system has no accept handler β feature is incompleteserver/src/services/socketHandlers/tradeHandlers.ts
The entire file (135 lines) contains only trade:create-offer. There is no trade:accept-offer, trade:decline-offer, or trade:cancel-offer handler. Trade offers are created in the DB with status 'pending' and notifications sent, but can never be completed. Items are never actually transferred.
Impact: If the trade UI is accessible to users, the feature is broken.
Fix: Implement accept/decline/cancel handlers, or disable the trade UI.
MEDIUM
M1: First 8 characters of JWT_SECRET leaked into NPC Matrix passwordserver/src/services/shopkeeperTools.ts:368
const password = `rinley-npc-${process.env.JWT_SECRET?.slice(0, 8) ?? 'campus'}`
Anyone with Synapse admin access can view NPC account passwords, gaining partial knowledge of the JWT signing secret.
Fix: Use a separate secret or derive via HMAC.
M2: Matrix admins receive isAdmin=true in client API responseserver/src/routes/auth.ts:87-91
formatStudentResponse checks Matrix server admin status and sets isAdmin = true in the response. Server-side middleware correctly blocks Matrix-based admin claims (the "aethrix mute-spree incident" at line 205-208), but the client receives the admin flag, exposing admin-only UI elements for operations that will be server-rejected.
Fix: Remove isMatrixServerAdmin from client response or use a separate flag.
M3: No security headersserver/src/index.ts (middleware section)
No helmet, no CSP, no X-Frame-Options, no X-Content-Type-Options, no HSTS. Responses lack protections against clickjacking, MIME sniffing, and XSS reflection.
Fix: Add helmet middleware with appropriate CSP.
M4: Faculty can kick/ban other faculty and adminsserver/src/services/socketHandlers/facultyPanelHandlers.ts:259-351
Kick, timeout, and ban handlers check the actor's faculty role but never check the target's role. A faculty member can ban another faculty member or an admin.
Fix: Add target role check β faculty cannot affect equal or higher privilege users.
M5: No per-student message cap on agent conversationsserver/src/services/socketHandlers/agentHandlers.ts
Each npc:message triggers an LLM API call with no daily or hourly cap per student. A student (or bot) could generate unlimited API costs. Tool execution has a 30/min rate limit, but the conversation itself does not.
Fix: Add a per-student daily message cap or token budget.
LOW
L1: Foreign keys without ON DELETE cause agent deletion failures
Multiple migrations (193, 223, 303)
deleteAgent() at agent.ts:511 runs DELETE FROM agents WHERE id = $1 without cleaning up dependent tables. FK constraints without ON DELETE CASCADE cause the delete to fail silently if the agent has conversations, skills, or custom agent data.
Fix: Add ON DELETE CASCADE to FKs or clean up dependent tables before delete.
L2: Agent job payments without transactionserver/src/services/agentAutonomy.ts:1704-1740
processJobPayments() updates agents.gem_balance and agent_jobs.total_gems_earned in separate queries without a transaction. A crash between queries creates a permanent accounting inconsistency. Virtual currency only.
Fix: Wrap both updates in a single transaction.
Patterns to Grep For
These patterns recur across the codebase. The confirmed findings above are specific instances, but the team should search for additional occurrences:
Non-atomic multi-step operations:
debitGems()followed by another operation without a shared transaction. Search fordebitGemsandcreditGemscalls that aren't insideBEGIN/COMMIT.Missing
socket.off()beforesocket.on(): 12 Zustand stores register socket listeners without cleanup. While socket replacement mitigates the worst case, search client stores forsocket.on(without a correspondingsocket.off(.parseInt()without NaN guard: Multiple routes useparseInt(req.params.xxx, 10)without checking for NaN. Search server routes forparseInt(req.params.Environment variable fallbacks that disable security: The
JWT_SECRET ?? ''andSCHOOL_WEBHOOK_SECRET || ''patterns both silently degrade security when env vars are missing. Search for|| ''and?? ''in security-critical code.
False Positives Identified by Red Team
These findings were reported by the initial scan but confirmed as NOT bugs by adversarial review. Documenting them for transparency:
Editor asset upload no auth β Route IS behind
authMiddleware + requireAdminat mount point (index.ts:339). The route file itself doesn't show auth, but Express applies the mount-level middleware.Nested transaction double-credit (gems.ts) β Uses separate DB connections, not nested transactions. No double-credit is possible. There is a minor atomicity concern (gems credited on one connection while subscription upsert on another), but the claimed "double-credit" mechanism does not exist.
Lecture exports no authorization β
authMiddlewareIS applied to all lecture routes vialectureRouter.use(authMiddleware). Missing instructor-only check is a lesser concern, not "no authorization."Missing try/catch in job handlers β pg-boss catches handler exceptions automatically and has retry configured (
retryLimit: 2). The missing try/catch is a style inconsistency, not a functional bug.
Dependency Vulnerabilities
47 HIGH/CRITICAL in package-lock.json (Trivy scan):
| Package | CVEs | Fix |
|---|---|---|
| axios 1.13.5 | CVE-2026-42033, CVE-2026-42035 (prototype pollution) | Upgrade to 1.15.1 |
| @grpc/grpc-js 1.14.3 | CVE-2026-48068, CVE-2026-48069 (crash on malformed request) | Upgrade to 1.14.4 |
| + 43 others | various HIGH | Run npm audit fix |
Recommendations (priority order)
- Today: Fix lecture instructor_id type mismatch (C1) β lectures are broken now
- Today: Set
SCHOOL_WEBHOOK_SECRETin production and addNODE_ENVguard (C2) - This week: Make JWT_SECRET startup fatal when empty (H1)
- This week: Add backoff to WebSocket reconnection (H2)
- This week: Wrap gem debit + property creation in a transaction (H3)
- This week: Add
helmetmiddleware (M3) - This week: Add per-student message cap on agent conversations (M5)
- Soon: Rotate JWT_SECRET if NPC password has been visible in Synapse admin (M1)
- Soon: Update axios and @grpc/grpc-js
- Backlog: Fix trade system or disable trade UI (H4)
- Backlog: Add target role check to faculty actions (M4)
- Backlog: Clean up FK cascades and agent deletion (L1, L2)
Architecture Map
See attached architecture_map.md for full system documentation (stack, 75 route files, 120 tables, auth flow, external services, AI agent system, deploy pipeline).
Methodology
| Phase | What | Findings | After Red Team |
|---|---|---|---|
| Security scan | Trivy + Semgrep (10 rulesets, 925 files) | 8 | 5 confirmed |
| Functional R1 | Core server routes, transactions, services | 11 | 4 confirmed |
| Functional R2 | Client stores, WebSocket, lectures, moderation | 12 | 4 confirmed |
| Functional R3 | Data consistency, rate limiting, Matrix, prompt injection | 13 | 4 confirmed |
| Red team R1-2 | Adversarial validation of rounds 1-2 | β | 9/15 confirmed, 2 FP, 4 overstated |
| Red team R3 | Adversarial validation of round 3 | β | 4/13 confirmed, 2 FP, 7 overstated |
| Total | 3 rounds + 2 red teams | 30 raw | 13 confirmed |
43% survival rate through adversarial review. Every finding in this report has been verified against the actual source code by an independent reviewer.
All findings are from source code review. No live system testing was performed. Nothing auto-submits β human review required before any action.
β Nexus, Liberation Labs / Transparent Humboldt Coalition