10 KiB
10 KiB
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
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.sidset for public pages withHttpOnlyonly.- Missing:
Secure,SameSite
CORS
- No CORS headers observed on public endpoints.
Caching
- HTML responses do not set explicit
Cache-Control. /app.jsusesCache-Control: public, max-age=0.
Error handling
- 404 returns default Express
Cannot GET /pathpage (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 labeledadminin plugin)
Admin endpoints:
- All
/admin/*routes protected byrequireRole("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
.mapfiles exposed. - No hardcoded secrets found in
src/web/public/app.js.
9) Node/Express Pitfalls Review
- No
helmetusage for security headers. express-sessioncookie flags missing.- No CSRF protection.
- No session ID rotation after OAuth login.
X-Powered-Byheader enabled.web.mountdoes not enforcenavItem.rolefor plugin routes.- Dependency audit:
npmnot 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
- 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/installwith a malicious repo URL. - Visit the page while logged in; request succeeds without CSRF token.
- Session cookie missing Secure/SameSite
- Run
curl -I https://lumi.ookamikun.tv/. - Observe
Set-Cookie: connect.sid=...; Path=/; HttpOnlywithoutSecure/SameSite.
- Session fixation risk
- Start a session and capture the
connect.sidcookie. - Complete OAuth login; the session ID remains the same (no regeneration).
- Plugin route role not enforced
- Visit
https://lumi.ookamikun.tv/plugins/sample-pluginwhile unauthenticated. - The page loads (200) despite being labeled
role: adminin the plugin.
- 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
--- 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
--- 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
--- 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
--- 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
--- 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
- Add CSRF protections for all state-changing routes.
- Enforce Secure/SameSite cookie flags for
connect.sid. - Rotate session IDs after OAuth login to prevent fixation.
- Add helmet (CSP/XFO/XCTO/Referrer-Policy/Permissions-Policy).
- Enforce
navItem.rolefor plugin routes.
Verification Checklist (Post-fix)
curl -I https://lumi.ookamikun.tv/shows CSP, XFO/frame-ancestors, XCTO, Referrer-Policy, Permissions-Policy.Set-CookieincludesSecureandSameSite=Lax.- OAuth login changes the session ID.
- CSRF token required for POST to
/admin/*endpoints. /plugins/sample-pluginis no longer accessible without auth when role isadmin.