fix(api): hash API keys in the v1 models catalog cache key

Validated in local merge-train-equivalent focused gate on release/v3.8.50 tip 9081b57146: catalog fingerprint regression + existing catalog-cache callers, 8 tests passed.
This commit is contained in:
Ravi Tharuma
2026-08-17 14:50:52 +02:00
committed by GitHub
parent 9081b57146
commit 611466b419
3 changed files with 25 additions and 1 deletions

View File

@@ -0,0 +1 @@
- **fix(api):** hash API keys in the `/v1/models` catalog cache Map key so heap dumps cannot leak bearer tokens (`src/app/api/v1/models/catalogCache.ts`)

View File

@@ -12,11 +12,19 @@
* Auth rejection is NOT handled here and must stay in the caller: it depends on
* live per-request state (dashboard cookie, API key) and must never be cached.
*/
import { createHash } from "node:crypto";
import { getModelCatalogCacheVersion } from "@/lib/db/readCache";
import { extractApiKey } from "@/sse/services/auth";
import { isCodexModelCatalogClient } from "./catalogRequest";
/** Fingerprint an API key for the catalog memo Map. Never store the raw secret. */
export function fingerprintCatalogAuthKey(apiKey: string): string {
if (!apiKey) return "";
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
}
export type CachedCatalog = {
body: string;
headers: Record<string, string>;
@@ -96,7 +104,7 @@ function buildCatalogCacheKey(
const configuredOnly = url.searchParams.get("configuredOnly") === "true" ? "1" : "0";
const hideAuto = catalogSettings?.hideAutoCombos ? "1" : "0";
const hideNoThink = catalogSettings?.hideNoThinkVariants ? "1" : "0";
return `${prefix}|${isCodex}|${apiKey}|${configuredOnly}|${hideAuto}|${hideNoThink}`;
return `${prefix}|${isCodex}|${fingerprintCatalogAuthKey(apiKey)}|${configuredOnly}|${hideAuto}|${hideNoThink}`;
}
// Tracks the model-catalog cache version (src/lib/db/readCache.ts) as of the last

View File

@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { fingerprintCatalogAuthKey } from "../../src/app/api/v1/models/catalogCache.ts";
test("fingerprintCatalogAuthKey never returns the raw API key", () => {
const raw = "sk-test-super-secret-catalog-key";
const finger = fingerprintCatalogAuthKey(raw);
assert.equal(finger.length, 16);
assert.equal(finger.includes(raw), false);
assert.equal(finger.includes("sk-test"), false);
assert.equal(fingerprintCatalogAuthKey(raw), finger);
assert.notEqual(fingerprintCatalogAuthKey("sk-other"), finger);
assert.equal(fingerprintCatalogAuthKey(""), "");
});