From 510e599af00e689aaa6c20f285fc4a026c6247a1 Mon Sep 17 00:00:00 2001 From: vishal kumar <144935874+vishu9334@users.noreply.github.com> Date: Thu, 17 Sep 2026 09:28:51 +0530 Subject: [PATCH] fix: prevent privilege escalation via client-supplied role in request body --- .vscode/settings.json | 9 +++++++++ src/app.test.ts | 11 ++++++++++- src/app.ts | 9 +++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2e74764 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "runql.ai.source": "githubCopilot", + "runql.ai.extension": "", + "runql.ai.apiProvider": "", + "runql.ai.model": "gpt-4.1", + "runql.ai.apiBaseUrl": "", + "runql.ai.sendSchemaContext": true, + "runql.ai.maxSchemaChars": 150000 +} diff --git a/src/app.test.ts b/src/app.test.ts index c4810de..724c2d7 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -5,7 +5,7 @@ describe("planted API bugs", () => { test.fails("rejects inactive keys", () => { expect(authenticate("revoked-token")).toBeNull(); }); - test.fails("does not trust admin role from body", async () => { + test("does not trust admin role from body", async () => { const app = buildApp(); const res = await app.inject({ method: "POST", @@ -14,6 +14,15 @@ describe("planted API bugs", () => { }); expect(res.json().ok).toBe(false); }); + test("does not trust alternative admin flags in body", async () => { + const app = buildApp(); + const res = await app.inject({ + method: "POST", + url: "/api/admin/users", + payload: { isAdmin: true, Role: "admin", roles: ["admin"], name: "Alice" }, + }); + expect(res.json().ok).toBe(false); + }); test("server responds", async () => { const app = buildApp(); const res = await app.inject({ method: "POST", url: "/api/tokens" }); diff --git a/src/app.ts b/src/app.ts index c32ceab..fd38fcd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,6 +6,12 @@ const configSchema = z.object({ name: z.string(), config: z.object({ retries: z.number().optional() }).strip(), }); +const userPayloadSchema = z + .object({ + name: z.string().optional(), + email: z.string().optional(), + }) + .strip(); export function buildApp() { const app = Fastify(); app.setErrorHandler((err: Error, _, reply) => @@ -16,8 +22,7 @@ export function buildApp() { requireAuth(req.headers.authorization?.replace("Bearer ", "")), ); app.post("/api/admin/users", async (req) => { - const body = req.body as any; - if (body.role === "admin") return { ok: true, granted: "admin" }; + const body = userPayloadSchema.parse(req.body ?? {}); return { ok: false }; }); app.post("/api/webhooks/retry", async (req) => {