From 97d83c7d1717db96dcdf5a03a5cc385f2ef698e1 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:36:06 -0700 Subject: [PATCH] fix(auth): block mcp token from app role gates --- src/api/routes.ts | 6 +++++- test/unit/routes-kill-switch.test.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index 0c7fc967cb..6a82c37190 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -4948,7 +4948,11 @@ async function getRoleSummaryForIdentity(env: Env, identity: AuthIdentity) { async function requireAppRole(c: ProtectedRouteContext, allowedRoles: ControlPanelRoleName[]): Promise { const identity = await authenticateRequestIdentity(c); if (!identity) return c.json({ error: "unauthorized" }, 401); - if (identity.kind !== "session") return null; + if (identity.kind !== "session") { + // GITTENSORY_MCP_TOKEN is a shared end-user credential; it must not satisfy app-role gates implicitly. + if (identity.actor === "mcp") return c.json({ error: "insufficient_role" }, 403); + return null; + } const summary = await loadControlPanelRoleSummary(c.env, identity.actor); return summary.roles.some((role) => allowedRoles.includes(role)) ? null : c.json({ error: "insufficient_role" }, 403); } diff --git a/test/unit/routes-kill-switch.test.ts b/test/unit/routes-kill-switch.test.ts index 1843b44093..90313e4917 100644 --- a/test/unit/routes-kill-switch.test.ts +++ b/test/unit/routes-kill-switch.test.ts @@ -57,6 +57,20 @@ describe("kill-switch operator route (#2359)", () => { expect(res.status).toBe(401); }); + it("rejects the shared MCP token without changing the global kill-switch", async () => { + const app = createApp(); + const env = createTestEnv(); + const headers = { authorization: `Bearer ${env.GITTENSORY_MCP_TOKEN}`, "content-type": "application/json" }; + + const read = await app.request("/v1/app/kill-switch", { headers }, env); + expect(read.status).toBe(403); + + const write = await app.request("/v1/app/kill-switch", { method: "POST", headers, body: JSON.stringify({ frozen: true }) }, env); + expect(write.status).toBe(403); + await expect(getGlobalAgentFrozenState(env)).resolves.toMatchObject({ frozen: false, updatedBy: null }); + expect(setGlobalAgentFrozen).not.toHaveBeenCalled(); + }); + it("GET surfaces a clear 503 (never a falsely reassuring unfrozen) when the singleton row is missing", async () => { const app = createApp(); const env = createTestEnv();