Project-Rivet / context /audit_report.md
HumboldtJoker's picture
Upload folder using huggingface_hub
4554903 verified
|
Raw
History Blame Contribute Delete
12.6 kB

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 broken
server/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 unset
server/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 secret
server/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 loop
client/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 purchase
server/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 incomplete
server/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 password
server/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 response
server/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 headers
server/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 admins
server/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 conversations
server/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 transaction
server/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:

  1. Non-atomic multi-step operations: debitGems() followed by another operation without a shared transaction. Search for debitGems and creditGems calls that aren't inside BEGIN/COMMIT.

  2. Missing socket.off() before socket.on(): 12 Zustand stores register socket listeners without cleanup. While socket replacement mitigates the worst case, search client stores for socket.on( without a corresponding socket.off(.

  3. parseInt() without NaN guard: Multiple routes use parseInt(req.params.xxx, 10) without checking for NaN. Search server routes for parseInt(req.params.

  4. Environment variable fallbacks that disable security: The JWT_SECRET ?? '' and SCHOOL_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:

  1. Editor asset upload no auth β€” Route IS behind authMiddleware + requireAdmin at mount point (index.ts:339). The route file itself doesn't show auth, but Express applies the mount-level middleware.

  2. 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.

  3. Lecture exports no authorization β€” authMiddleware IS applied to all lecture routes via lectureRouter.use(authMiddleware). Missing instructor-only check is a lesser concern, not "no authorization."

  4. 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)

  1. Today: Fix lecture instructor_id type mismatch (C1) β€” lectures are broken now
  2. Today: Set SCHOOL_WEBHOOK_SECRET in production and add NODE_ENV guard (C2)
  3. This week: Make JWT_SECRET startup fatal when empty (H1)
  4. This week: Add backoff to WebSocket reconnection (H2)
  5. This week: Wrap gem debit + property creation in a transaction (H3)
  6. This week: Add helmet middleware (M3)
  7. This week: Add per-student message cap on agent conversations (M5)
  8. Soon: Rotate JWT_SECRET if NPC password has been visible in Synapse admin (M1)
  9. Soon: Update axios and @grpc/grpc-js
  10. Backlog: Fix trade system or disable trade UI (H4)
  11. Backlog: Add target role check to faculty actions (M4)
  12. 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