Delete security-audit-report.md
This commit is contained in:
parent
f2dfffe901
commit
f877e4f084
@ -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`.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user