Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit7b0a81b

security: fix auth bypass on /admin/security and /admin/soc2 (both were publicly readable)

security: fix auth bypass on /admin/security and /admin/soc2 (both were publicly readable)

CRITICAL. adminSecurity.use("/admin/security*", ...) -- no slash before
the * -- does not match "/admin/security" itself in this Hono version.
Confirmed via a standalone reproduction (Hono app with just this
pattern: middleware never fires for the bare path) and, worse, live in
production: an unauthenticated GET to both /admin/security and
/admin/soc2 returned a real 200 with the full SOC 2 evidence dashboard
rendered -- failed logins, locked/deleted accounts, admin action log,
MFA enrollment status, active session counts. No login required. This
has been live since these routes shipped; found via a full production
HTTP sweep across all 201 static page routes, not a doc, not a prior
test -- there was no test file for this route at all.

Same broken pattern is used in 12 other files for other auth gates
(org-secrets.tsx already fixed separately; deploy-targets.tsx,
tokens.tsx, settings-*.tsx, pulse.tsx, debt-map.tsx,
cross-repo-search.tsx, push-notifications.tsx, stale-branches.tsx
still need the same fix -- follow-up commits, this one ships alone
given severity).

Fixed by registering the guard on the exact path AND the "/path/*"
sub-path form separately -- verified-correct pattern, confirmed against
the actual Hono version in this repo before shipping (not assumed).
Added the test file that should have existed from day one.
ccantynz-alt committed on July 20, 2026Parent: ad189d6
2 files changed+44107b0a81b40d2009c123e46cb246859f8580b8698b
2 changed files+44−10
Addedsrc/__tests__/admin-security.test.ts+28−0View fileUnifiedSplit
1/**
2 * Regression coverage for a real production security bug: the auth guard
3 * on /admin/security and /admin/soc2 was registered with a glued wildcard
4 * pattern ("/admin/security*", no slash before the *) that doesn't match
5 * in this Hono version -- confirmed via a standalone Hono reproduction and
6 * live: an unauthenticated GET to both pages returned a real 200 with the
7 * SOC 2 evidence dashboard (failed logins, locked accounts, MFA status,
8 * active sessions, account deletions) in production. Found via a full
9 * production HTTP sweep, not a doc or a prior test -- there was no test
10 * file for this route at all before this one.
11 */
12import { describe, it, expect } from "bun:test";
13
14describe("admin-security — auth gate", () => {
15 it("GET /admin/security without auth redirects to login, not 200", async () => {
16 const app = (await import("../app")).default;
17 const res = await app.request("/admin/security");
18 expect(res.status).toBe(302);
19 expect(res.headers.get("location") || "").toContain("/login");
20 });
21
22 it("GET /admin/soc2 without auth redirects to login, not 200", async () => {
23 const app = (await import("../app")).default;
24 const res = await app.request("/admin/soc2");
25 expect(res.status).toBe(302);
26 expect(res.headers.get("location") || "").toContain("/login");
27 });
28});
Modifiedsrc/routes/admin-security.tsx+16−10View fileUnifiedSplit
99 */
1010
1111import { Hono } from "hono";
12import type { Context } from "hono";
1213import { and, count, desc, eq, gte, sql } from "drizzle-orm";
1314import { db } from "../db";
1415import {
2728adminSecurity.use("*", softAuth);
2829
2930// ── Auth guard ───────────────────────────────────────────────────────────────
30adminSecurity.use("/admin/security*", async (c, next) => {
31// SECURITY: "/admin/security*" (no slash before the *) does not match "/admin/security"
32// itself in this Hono version -- confirmed via a standalone reproduction, and
33// live: an unauthenticated GET to both pages returned 200 in production. This
34// gate was NEVER running. Registering on the exact path AND the "/*" sub-path
35// form is the verified-correct pattern (see the sibling fix in org-secrets.tsx).
36const securityGuard = async (
37 c: Context<AuthEnv>,
38 next: () => Promise<void>
39) => {
3140 const user = c.get("user");
3241 if (!user || !(await isSiteAdmin(user.id))) {
33 return c.redirect("/login?redirect=/admin/security");
42 return c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`);
3443 }
3544 return next();
36});
37adminSecurity.use("/admin/soc2*", async (c, next) => {
38 const user = c.get("user");
39 if (!user || !(await isSiteAdmin(user.id))) {
40 return c.redirect("/login?redirect=/admin/soc2");
41 }
42 return next();
43});
45};
46adminSecurity.use("/admin/security", securityGuard);
47adminSecurity.use("/admin/security/*", securityGuard);
48adminSecurity.use("/admin/soc2", securityGuard);
49adminSecurity.use("/admin/soc2/*", securityGuard);
4450
4551// ── Scoped CSS ───────────────────────────────────────────────────────────────
4652const securityStyles = `
4753