diff --git a/src/auth/security.ts b/src/auth/security.ts index f27c4bb74..3ee0f3a0a 100644 --- a/src/auth/security.ts +++ b/src/auth/security.ts @@ -116,7 +116,10 @@ export async function authenticateSessionToken(env: Env, token: string | undefin if (!token) return null; const session = await getAuthSessionByTokenHash(env, await hashToken(token)); if (!session) return null; - if (session.revokedAt || Date.parse(session.expiresAt) <= Date.now()) return null; + // Fail closed on an unparseable expiry: Date.parse → NaN makes `NaN <= Date.now()` false, which would + // otherwise authenticate a session whose stored expires_at is malformed/empty as if it never expired. + const expiresAtMs = Date.parse(session.expiresAt); + if (session.revokedAt || !Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) return null; await touchAuthSession(env, session.id); return { kind: "session", actor: session.login, session }; } diff --git a/test/unit/auth.test.ts b/test/unit/auth.test.ts index 98833e887..fa1f94619 100644 --- a/test/unit/auth.test.ts +++ b/test/unit/auth.test.ts @@ -26,6 +26,11 @@ describe("private-beta auth and rate limiting", () => { const expired = await createSessionForGitHubUser(env, { login: "expired-user" }); await env.DB.prepare("update auth_sessions set expires_at = ? where login = ?").bind("2020-01-01T00:00:00.000Z", "expired-user").run(); await expect(authenticatePrivateToken(env, expired.token)).resolves.toBeNull(); + + // Fail closed when the stored expiry is unparseable (NaN), not authenticate it as a never-expiring session. + const malformed = await createSessionForGitHubUser(env, { login: "malformed-expiry-user" }); + await env.DB.prepare("update auth_sessions set expires_at = ? where login = ?").bind("not-a-date", "malformed-expiry-user").run(); + await expect(authenticatePrivateToken(env, malformed.token)).resolves.toBeNull(); }); it("handles auth helper fallbacks for cookies, login lists, and token comparison", async () => {