From d8e18e4bb139f19b5e7e4cba0ca743030d936ff0 Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:51:02 +0900 Subject: [PATCH] fix(server): reject future-dated OAuth state tokens --- .../src/services/oauth/state-manager.test.ts | 22 ++++++++++++++++++- .../src/services/oauth/state-manager.ts | 6 +++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/oauth/state-manager.test.ts b/packages/server/src/services/oauth/state-manager.test.ts index cc145ceb..c17ced35 100644 --- a/packages/server/src/services/oauth/state-manager.test.ts +++ b/packages/server/src/services/oauth/state-manager.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createOAuthState, @@ -16,6 +16,26 @@ describe("oauth state manager", () => { } ); + it("rejects a signed state created in the future", async () => { + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date("2026-09-26T10:00:00.000Z")); + const state = await createOAuthState("test-secret", { + organizationId: "org_123", + provider: "github", + redirectTo: "/settings", + }); + vi.setSystemTime(new Date("2026-09-26T09:59:59.000Z")); + + await expect(validateOAuthState("test-secret", state)).resolves.toEqual({ + error: "Invalid OAuth state timestamp", + valid: false, + }); + } finally { + vi.useRealTimers(); + } + }); + it("rejects state tokens with extra separators", async () => { const state = await createOAuthState("test-secret", { organizationId: "org_123", diff --git a/packages/server/src/services/oauth/state-manager.ts b/packages/server/src/services/oauth/state-manager.ts index 89e86c57..c54d84cf 100644 --- a/packages/server/src/services/oauth/state-manager.ts +++ b/packages/server/src/services/oauth/state-manager.ts @@ -162,6 +162,12 @@ export async function validateOAuthState( } const age = Date.now() - parsed.createdAt; + // A signed state must have a finite creation time that is not in the future. + // Otherwise a future timestamp can extend the token beyond its TTL. + if (!Number.isFinite(age) || age < 0) { + return { error: "Invalid OAuth state timestamp", valid: false }; + } + if (age > ttlMs) { return { error: `OAuth state expired (age: ${Math.round(age / 1000)}s, max: ${Math.round(ttlMs / 1000)}s)`,