From 3ab540a05a13eb7e3ea01bdb0c43859e3630ccf8 Mon Sep 17 00:00:00 2001 From: jony376 Date: Fri, 29 May 2026 02:20:06 -0700 Subject: [PATCH] fix(auth): trim bearer tokens in rate-limit keys Co-authored-by: Cursor --- src/auth/rate-limit.ts | 6 +++--- test/unit/auth.test.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/auth/rate-limit.ts b/src/auth/rate-limit.ts index 7bad0a92a4..f3810bf2ad 100644 --- a/src/auth/rate-limit.ts +++ b/src/auth/rate-limit.ts @@ -1,7 +1,7 @@ import type { Context } from "hono"; import { DurableObject } from "cloudflare:workers"; import { recordAuditEvent } from "../db/repositories"; -import { hashToken } from "./security"; +import { extractBearerToken, hashToken } from "./security"; export type RateLimitClass = "strict" | "normal" | "expensive"; @@ -110,7 +110,7 @@ export function routeClassForPath(path: string): RateLimitClass { } async function rateLimitKey(c: Context<{ Bindings: Env }>, routeClass: RateLimitClass): Promise { - const token = c.req.header("authorization")?.replace(/^Bearer\s+/i, ""); + const token = extractBearerToken(c.req.header("authorization")); const ip = c.req.header("cf-connecting-ip") ?? c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown-ip"; const pathGroup = c.req.path.replace(/\/\d+(?=\/|$)/g, "/:number").replace(/\/[^/]+\/[^/]+\/pulls\//, "/:owner/:repo/pulls/"); const identity = token ? `token:${await hashToken(token)}` : `ip:${await hashToken(ip)}`; @@ -118,7 +118,7 @@ async function rateLimitKey(c: Context<{ Bindings: Env }>, routeClass: RateLimit } async function actorHint(c: Context<{ Bindings: Env }>): Promise { - const token = c.req.header("authorization")?.replace(/^Bearer\s+/i, ""); + const token = extractBearerToken(c.req.header("authorization")); if (!token) return "anonymous"; return `token:${(await hashToken(token)).slice(0, 16)}`; } diff --git a/test/unit/auth.test.ts b/test/unit/auth.test.ts index 746fe26bd4..c2afe94e8e 100644 --- a/test/unit/auth.test.ts +++ b/test/unit/auth.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createSessionFromGitHubToken, pollGitHubDeviceFlow, startGitHubDeviceFlow } from "../../src/auth/github-oauth"; import { enforceRateLimit, RateLimiter, routeClassForPath } from "../../src/auth/rate-limit"; -import { authenticatePrivateToken, createSessionForGitHubUser, revokeSession } from "../../src/auth/security"; +import { authenticatePrivateToken, createSessionForGitHubUser, extractBearerToken, hashToken, revokeSession } from "../../src/auth/security"; import { createTestEnv } from "../helpers/d1"; describe("private-beta auth and rate limiting", () => { @@ -68,6 +68,13 @@ describe("private-beta auth and rate limiting", () => { expect(routeClassForPath("/v1/repos")).toBe("normal"); }); + it("hashes trimmed bearer tokens for rate-limit identity", async () => { + const padded = extractBearerToken("Bearer session-token "); + const plain = extractBearerToken("Bearer session-token"); + expect(padded).toBe(plain); + expect(await hashToken(padded!)).toEqual(await hashToken(plain!)); + }); + it("enforces route limits with session and IP keys plus retry headers", async () => { const env = createTestEnv(); const noLimiter = fakeContext(env, "/v1/repos/123/pulls/456", { authorization: "Bearer session-token" });