# Security Policy ## Reporting a Vulnerability If you discover a security vulnerability in LobbyForge, **please do not open a public GitHub issue**. Instead, report it privately: 1. **GitHub Security Advisories** (preferred): go to the [Security tab](../../security/advisories/new) of this repo and create a private security advisory. This is monitored and reaches the maintainers directly. 2. **GitHub Confidential Vulnerability Reporting**: use the "Report a vulnerability" button on the Security tab. Please include: - A description of the vulnerability and its impact. - Steps to reproduce (proof of concept). - The affected version/commit. We acknowledge reports within **48 hours** and aim to publish a fix or mitigation within **30 days** for critical issues. ## Supported Versions There are no tagged releases yet (experimental alpha); report vulnerabilities against the current `main` branch. Self-hosted instances should keep up to date via the built-in update system (Admin → Updates). ## Security Measures LobbyForge implements defense-in-depth: - **Session cookies**: HMAC-SHA256 signed, HttpOnly, SameSite=Lax, Secure in production. web and the realtime gateway refuse to start with a missing, short or published placeholder session secret (only the local dev stack, with `LOBBYFORGE_DEV_STACK=true`, may use one); the dev stack binds `127.0.0.1` only. - **Image uploads**: content-sniffed format and dimensions (every GIF frame inside the image), a per-user stored quota, a per-address daily byte budget, and no uploads for guest accounts. - **Desktop app**: the window only shows the connected instance and its own connect screen; other pages open in the system browser. - **Guest IDs**: 128-bit cryptographic randomness (`crypto.randomBytes`). - **Admin auth**: owner session or constant-time emergency token (≥32 chars). - **CSRF protection**: Fetch Metadata headers + Origin validation on all mutations. - **CSP**: strict `script-src 'self'`, `object-src 'none'`, `frame-ancestors 'none'`. `frame-src` allows exactly one origin, `https://www.youtube-nocookie.com` — the Watch Party player (framing only, sandboxed; see `docs/WATCH_PARTY.md`). - **Request body limits**: per-route caps (default 1 MiB). - **Rate limiting**: Redis-backed (fails closed), per-route limits. - **Update signatures**: Ed25519-signed manifests required for self-host updates. - **Input validation**: Zod schemas on every API route. - **TURN relay credentials**: per-user, time-limited (coturn REST auth) — no permanent shared TURN secret exists. ## Marketplace plugins — trust model (ADR-001) **Current model: reviewed-only.** The marketplace accepts curated, admin-reviewed plugins. The plugin-worker executes them in a hardened container (child process, strict IPC, process-group kill, memory/pids caps, read-only fs, internal network). This is adequate for **reviewed code where the review process is the trust gate**. **NOT a hostile-code sandbox.** Arbitrary untrusted JavaScript requires per-plugin containers with separate UIDs, PID namespaces and cgroups. Until that exists, the marketplace requires human review before approval, and dynamic execution stays opt-in. See docs/ARCHITECTURE_DECISIONS.md for the full ADR. ## Marketplace plugins — isolated worker runtime (LF-SEC-010) Third-party plugin bundles execute in the **plugin-worker container**, never in the web process: - separate container, internal-only network (not behind nginx at all); - NO host secrets in its environment — only the RPC token and the storage-capability token; - read-only root filesystem + read-only plugin bundle mount; - `mem_limit 256m`, `pids_limit 64`, `cap_drop ALL`, `no-new-privileges`, healthcheck-gated restarts; - the web app calls it with hard timeouts — a hung plugin can at worst restart the worker, never the web app; - `ctx.storage.*` executes back on the host via the internal `/api/internal/plugin-storage` endpoint (404 at the public edge), hard-scoped to the plugin's own (serverId, pluginId) keyspace. Dynamic execution STILL defaults to off. Enabling requires BOTH `LOBBYFORGE_DYNAMIC_PLUGINS_ENABLED=true` AND `LOBBYFORGE_PLUGIN_WORKER_URL` — the flag alone loads nothing (fail closed), and the old in-process import path for third-party code has been removed from the loader entirely. Workspace plugins (hushle, quiz, …) remain trusted first-party code in the static registry. ## Marketplace plugins — dynamic execution is DISABLED (SEC-008) Third-party plugin bundles (`plugins/installed/…`) are NOT executed by default. Two independent gates must BOTH agree before any dynamic plugin code runs, and both default to closed: 1. `POST /api/marketplace/install` refuses (503) unless `LOBBYFORGE_DYNAMIC_PLUGINS_ENABLED=true`. 2. The boot-time loader (`lib/plugin-loader.ts`) skips the `plugins/installed/` walk entirely without the same flag. (Historical note — superseded by the isolated worker runtime above.) The original in-process execution model is gone; the remaining gate below is defense-in-depth while the feature stays opt-in: ## Self-Host Security Checklist When deploying LobbyForge on your own server: - [ ] Set `LOBBYFORGE_SESSION_SECRET` to ≥32 chars of random hex. - [ ] Set `LOBBYFORGE_ADMIN_TOKEN` to ≥32 chars (different from session secret). - [ ] Set `LOBBYFORGE_SETUP_TOKEN` and remove it after first-run setup. - [ ] Use HTTPS (certbot/Let's Encrypt) — WebRTC requires it. - [ ] Keep `registrationMode` on `invite_only` (the default). - [ ] Run the Doctor health check after deployment. - [ ] Configure backups: `node scripts/lfctl.mjs backup create --out backups --database-url ...` (see docs/BACKUP_DRILL.md — includes a destructive restore drill you should rehearse before trusting backups).