From f877e4f084c1179aba3a040ea6159ac03e438380 Mon Sep 17 00:00:00 2001 From: frarol96 Date: Sat, 30 May 2026 19:23:54 +0000 Subject: [PATCH] Delete security-audit-report.md --- security-audit-report.md | 286 --------------------------------------- 1 file changed, 286 deletions(-) delete mode 100644 security-audit-report.md diff --git a/security-audit-report.md b/security-audit-report.md deleted file mode 100644 index 4208dad..0000000 --- a/security-audit-report.md +++ /dev/null @@ -1,286 +0,0 @@ -# Lumi Bot Public Surface Security Audit - -Date: 2026-01-22 -Target: https://lumi.ookamikun.tv (unauthenticated/public surface) -Scope: Public pages, unauthenticated endpoints, and code review of the local repo. - -## 0) Guardrails -- No destructive actions performed. -- No authentication performed. -- Only safe, read-only checks and code review. - -## 1) Public Attack Surface Inventory - -### Discovered public URLs (unauthenticated) -- https://lumi.ookamikun.tv/ -- https://lumi.ookamikun.tv/commands -- https://lumi.ookamikun.tv/leaderboards -- https://lumi.ookamikun.tv/plugins/expression-interaction -- https://lumi.ookamikun.tv/plugins/sample-plugin - -### Authentication entrypoints -- https://lumi.ookamikun.tv/auth/discord (302 to Discord OAuth) -- https://lumi.ookamikun.tv/auth/twitch/login (302 to Twitch OAuth) - -### Admin endpoints (unauthenticated behavior) -- https://lumi.ookamikun.tv/admin (302 to /auth/discord) -- https://lumi.ookamikun.tv/admin/commands (302 to /auth/discord) -- https://lumi.ookamikun.tv/admin/settings (302 to /auth/discord) - -### Hidden endpoint probes (all 404) -- /api, /api/, /api/v1 -- /oauth, /callback, /webhook, /webhook/discord -- /health, /metrics, /status, /debug, /logs -- /swagger, /openapi, /graphql - -### Third-party integrations observed -- Discord OAuth (authorize, token, user fetch) via /auth/discord -- Twitch OAuth (authorize, token, user fetch) via /auth/twitch/login - -### JS/CSS assets -- https://lumi.ookamikun.tv/app.js -- https://lumi.ookamikun.tv/styles.css - -### Source maps -- /app.js.map and /styles.css.map return 404 (not exposed) - -## 2) Passive Misconfiguration Checks - -### TLS -- HTTP -> HTTPS redirect observed for http://lumi.ookamikun.tv/ -- HSTS present: `Strict-Transport-Security: max-age=63072000; preload` - -### Mixed Content -- No `http://` links detected in homepage HTML. - -### Security Headers (public HTML endpoints) -Observed on `/`, `/commands`, `/leaderboards`: -- Present: `Strict-Transport-Security` -- Present: `X-Powered-By: Express` -- Missing: `Content-Security-Policy`, `X-Frame-Options`/`frame-ancestors`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy` - -### Cookies -- `connect.sid` set for public pages with `HttpOnly` only. -- Missing: `Secure`, `SameSite` - -### CORS -- No CORS headers observed on public endpoints. - -### Caching -- HTML responses do not set explicit `Cache-Control`. -- `/app.js` uses `Cache-Control: public, max-age=0`. - -### Error handling -- 404 returns default Express `Cannot GET /path` page (no stack trace or env leaks). - -## 3) OAuth Flow Hardening (Discord/Twitch) - -Checklist: -- State present and random: Yes (`crypto.randomBytes(16)`) -- State validated on callback: Yes -- Rejects missing/invalid state: Yes -- Session binding: Uses session-stored state -- Session fixation protection: Missing (no session regeneration after login) -- Redirect handling: No open redirect parameter observed - -## 4) Access Control - -Public pages returning data: -- `/commands`: command list and counts (expected public?) -- `/leaderboards`: leaderboards and expression summary -- `/plugins/expression-interaction`: public plugin page with global stats -- `/plugins/sample-plugin`: public content (despite role labeled `admin` in plugin) - -Admin endpoints: -- All `/admin/*` routes protected by `requireRole("admin")`. - -## 5) CSRF and Cross-site Request Risks -- No CSRF middleware detected for POST routes (logout, profile updates, admin actions). -- Session cookies do not set `SameSite`, increasing CSRF risk. -- High-impact admin actions include plugin install/update, updates, and restart. - -## 6) Injection Testing (safe) -- No reflected parameters observed on public pages. -- User-controlled content in HTML uses EJS escaped output (`<%= ... %>`). -- Custom pages use raw HTML (`<%- page.content %>`), but creation is admin-gated. - -## 7) Rate Limiting and Enumeration -- No rate limiting middleware observed (e.g. `express-rate-limit`). -- Public endpoints (`/commands`, `/leaderboards`) are enumerable without throttling. - -## 8) Static Asset and Source Map Leakage -- No `.map` files exposed. -- No hardcoded secrets found in `src/web/public/app.js`. - -## 9) Node/Express Pitfalls Review -- No `helmet` usage for security headers. -- `express-session` cookie flags missing. -- No CSRF protection. -- No session ID rotation after OAuth login. -- `X-Powered-By` header enabled. -- `web.mount` does not enforce `navItem.role` for plugin routes. -- Dependency audit: `npm` not available in this environment, audit not executed. - -## Findings - -| Severity | Title | Affected URL/endpoint | Evidence | Impact | Fix | -| --- | --- | --- | --- | --- | --- | -| High | Missing CSRF protection on state-changing routes | Multiple POST endpoints (e.g. `/admin/*`, `/profile/*`, `/auth/logout`) | No CSRF middleware in `src/web/server.js` and no Origin/Referer checks | Logged-in admins can be forced to install plugins, change settings, or trigger updates | Add CSRF tokens and/or Origin checks; set `SameSite=Lax` cookies | -| Medium | Session cookie missing Secure and SameSite attributes | `/` (and other public pages) | `Set-Cookie: connect.sid=...; Path=/; HttpOnly` (no `Secure`/`SameSite`) | Session cookie can be sent over HTTP or cross-site requests; increases CSRF/session hijack risk | Configure `express-session` cookie flags; enable `trust proxy` when behind TLS terminator | -| Medium | Session fixation risk after OAuth login | `/auth/discord/callback`, `/auth/twitch/callback` | Session is populated without regeneration (`req.session.user = ...`) | Attacker may fixate session ID before login and reuse it after victim authenticates | Call `req.session.regenerate()` on successful OAuth login | -| Medium | Plugin route role not enforced | `/plugins/sample-plugin` (and any plugin using `web.mount`) | `web.mount` uses `app.use` without role guard; sample plugin labeled `admin` is public | Plugin pages intended for admins can be publicly accessible | Enforce `navItem.role` in `web.mount` with `requireRole` | -| Low | Missing baseline security headers | `/`, `/commands`, `/leaderboards` | No CSP/XFO/XCTO/Referrer-Policy/Permissions-Policy; `X-Powered-By: Express` | Increases exposure to clickjacking/XSS/mime sniffing and framework fingerprinting | Use `helmet` and disable `x-powered-by` | - -## Reproduction Steps - -1) Missing CSRF protection -- Log in as admin in a normal browser session. -- Host a page that auto-submits a POST form to `https://lumi.ookamikun.tv/admin/plugins/install` with a malicious repo URL. -- Visit the page while logged in; request succeeds without CSRF token. - -2) Session cookie missing Secure/SameSite -- Run `curl -I https://lumi.ookamikun.tv/`. -- Observe `Set-Cookie: connect.sid=...; Path=/; HttpOnly` without `Secure`/`SameSite`. - -3) Session fixation risk -- Start a session and capture the `connect.sid` cookie. -- Complete OAuth login; the session ID remains the same (no regeneration). - -4) Plugin route role not enforced -- Visit `https://lumi.ookamikun.tv/plugins/sample-plugin` while unauthenticated. -- The page loads (200) despite being labeled `role: admin` in the plugin. - -5) Missing security headers -- Run `curl -I https://lumi.ookamikun.tv/`. -- Confirm absence of CSP/XFO/XCTO/Referrer-Policy/Permissions-Policy headers. - -## Suggested Patches / Config Snippets - -### A) Session cookie hardening -```diff ---- a/src/web/server.js -+++ b/src/web/server.js -@@ - const app = express(); -+ app.set("trust proxy", 1); -+ app.disable("x-powered-by"); -@@ - app.use( - session({ - secret: ensureSessionSecret(), - resave: false, - saveUninitialized: false, -- store: sessionStore -+ store: sessionStore, -+ cookie: { -+ httpOnly: true, -+ secure: true, -+ sameSite: "lax" -+ } - }) - ); -``` - -### B) Add helmet with baseline headers -```diff ---- a/src/web/server.js -+++ b/src/web/server.js -@@ --const express = require("express"); -+const express = require("express"); -+const helmet = require("helmet"); -@@ - const app = express(); -+ -+ app.use( -+ helmet({ -+ contentSecurityPolicy: { -+ useDefaults: true, -+ directives: { -+ "script-src": ["'self'"], -+ "style-src": ["'self'", "'unsafe-inline'"] -+ } -+ }, -+ referrerPolicy: { policy: "strict-origin-when-cross-origin" } -+ }) -+ ); -``` - -### C) Rotate session ID after OAuth login -```diff ---- a/src/web/server.js -+++ b/src/web/server.js -@@ -- req.session.user = { -+ req.session.regenerate(() => { -+ req.session.user = { - id: profile.id, - username: profile.internal_username, - avatar: user.avatar, - roles, - ...flags -- }; -- req.session.discordToken = token; -- setFlash(req, "success", "Logged in."); -- res.redirect("/"); -+ }; -+ req.session.discordToken = token; -+ setFlash(req, "success", "Logged in."); -+ res.redirect("/"); -+ }); -``` - -### D) Enforce role protection for plugin mounts -```diff ---- a/src/web/server.js -+++ b/src/web/server.js -@@ -- mount: (mountPath, router, navItem) => { -- app.use(mountPath, router); -+ mount: (mountPath, router, navItem) => { -+ const role = navItem?.role || "public"; -+ const middleware = role && role !== "public" ? requireRole(role) : null; -+ if (middleware) { -+ app.use(mountPath, middleware, router); -+ } else { -+ app.use(mountPath, router); -+ } - if (navItem) { - navItems.push({ ...navItem, path: mountPath }); - } - }, -``` - -### E) CSRF protection for state-changing routes -```diff ---- a/src/web/server.js -+++ b/src/web/server.js -@@ -+const csrf = require("csurf"); -@@ - const app = express(); -+ const csrfProtection = csrf(); -@@ -- app.post("/auth/logout", (req, res) => { -+ app.post("/auth/logout", csrfProtection, (req, res) => { - req.session.destroy(() => { - res.redirect("/"); - }); - }); -``` -Add hidden `_csrf` fields in forms and/or apply CSRF middleware globally after session initialization. - -## Top 5 Fixes This Week -1) Add CSRF protections for all state-changing routes. -2) Enforce Secure/SameSite cookie flags for `connect.sid`. -3) Rotate session IDs after OAuth login to prevent fixation. -4) Add helmet (CSP/XFO/XCTO/Referrer-Policy/Permissions-Policy). -5) Enforce `navItem.role` for plugin routes. - -## Verification Checklist (Post-fix) -- `curl -I https://lumi.ookamikun.tv/` shows CSP, XFO/frame-ancestors, XCTO, Referrer-Policy, Permissions-Policy. -- `Set-Cookie` includes `Secure` and `SameSite=Lax`. -- OAuth login changes the session ID. -- CSRF token required for POST to `/admin/*` endpoints. -- `/plugins/sample-plugin` is no longer accessible without auth when role is `admin`. -