From a3daebc02f5b8c4e419d4609c9cb459411a1e2f0 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 22 Jul 2026 08:13:47 -0300 Subject: [PATCH] fix(security): use SHA-256 for Notion per-caller cache namespace hash (was 32-bit FNV) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security-review follow-up: the per-caller namespace hash is a security boundary (cross-tenant cache isolation), so a 32-bit FNV digest was too collision-prone — an attacker could craft a cookie colliding into a victim's namespace. SHA-256 (128-bit prefix) makes accidental + crafted collisions infeasible. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- open-sse/services/notionThreadSessions.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/open-sse/services/notionThreadSessions.ts b/open-sse/services/notionThreadSessions.ts index 4f6032ae34..2aaeac14eb 100644 --- a/open-sse/services/notionThreadSessions.ts +++ b/open-sse/services/notionThreadSessions.ts @@ -14,7 +14,7 @@ * - Optional client-supplied continuity via body (`notion_thread_id`/`thread_id`) * or the `X-Notion-Thread-Id` header (via `ExecuteInput.clientHeaders`). */ -import { randomUUID } from "node:crypto"; +import { randomUUID, createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -439,10 +439,10 @@ export function readClientThreadId( export function hashNotionCallerCookie(cookie: string): string { const raw = (cookie || "").trim(); if (!raw) return "anon"; - let hash = 0x811c9dc5; - for (let i = 0; i < raw.length; i++) { - hash ^= raw.charCodeAt(i); - hash = Math.imul(hash, 0x01000193) >>> 0; - } - return hash.toString(16).padStart(8, "0"); + // SHA-256 (128-bit prefix) rather than a 32-bit FNV digest: this hash is a + // SECURITY boundary (per-caller cache isolation), so it must be collision- + // resistant — a 32-bit space is birthday-crackable, letting an attacker craft + // a cookie that lands in a victim's namespace. crypto SHA-256 makes both + // accidental and crafted collisions infeasible; the raw cookie is never stored. + return createHash("sha256").update(raw).digest("hex").slice(0, 32); }