From 3a82510038eb7c8f854e43eb2d9197b763bcdd7a Mon Sep 17 00:00:00 2001 From: St0rmz1 Date: Tue, 17 Feb 2026 14:19:05 -0800 Subject: [PATCH 1/2] fix(auth): use constant-time comparison for internal API secret validation (AF-5) Replace short-circuit !== string comparison with timing-safe XOR-based comparison for INTERNAL_API_SECRET in kiloclaw to prevent theoretical timing side channel attacks --- kiloclaw/src/auth/middleware.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/kiloclaw/src/auth/middleware.ts b/kiloclaw/src/auth/middleware.ts index 65501823a..75b3383ab 100644 --- a/kiloclaw/src/auth/middleware.ts +++ b/kiloclaw/src/auth/middleware.ts @@ -104,9 +104,20 @@ export async function internalApiMiddleware(c: Context, next: Next) { return c.json({ error: 'Forbidden' }, 403); } - if (apiKey !== secret) { + if (!timingSafeEqual(apiKey, secret)) { return c.json({ error: 'Forbidden' }, 403); } return next(); } + +function timingSafeEqual(a: string, b: string): boolean { + if (a.length !== b.length) { + return false; + } + let result = 0; + for (let i = 0; i < a.length; i++) { + result |= a.charCodeAt(i) ^ b.charCodeAt(i); + } + return result === 0; +} From 22c9e96f7bbd03e550d9374ebdb66a58f34236be Mon Sep 17 00:00:00 2001 From: St0rmz1 Date: Tue, 17 Feb 2026 14:47:21 -0800 Subject: [PATCH 2/2] Replace short-circuit !== string comparison with crypto.subtle.timingSafeEqual for INTERNAL_API_SECRET in kiloclaw. Uses dummy self-comparison on length mismatch to avoid leaking secret length via timing side-channel. Adds vitest setup to polyfill crypto.subtle.timingSafeEqual for Node test env. --- kiloclaw/src/auth/middleware.ts | 14 ++++++++------ kiloclaw/src/test-setup.ts | 17 +++++++++++++++++ kiloclaw/vitest.config.ts | 1 + 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 kiloclaw/src/test-setup.ts diff --git a/kiloclaw/src/auth/middleware.ts b/kiloclaw/src/auth/middleware.ts index 75b3383ab..3deeb400a 100644 --- a/kiloclaw/src/auth/middleware.ts +++ b/kiloclaw/src/auth/middleware.ts @@ -112,12 +112,14 @@ export async function internalApiMiddleware(c: Context, next: Next) { } function timingSafeEqual(a: string, b: string): boolean { - if (a.length !== b.length) { + const encoder = new TextEncoder(); + const aBytes = encoder.encode(a); + const bBytes = encoder.encode(b); + + if (aBytes.length !== bBytes.length) { + // Compare a against itself so the timing is constant regardless of length mismatch + crypto.subtle.timingSafeEqual(aBytes, aBytes); return false; } - let result = 0; - for (let i = 0; i < a.length; i++) { - result |= a.charCodeAt(i) ^ b.charCodeAt(i); - } - return result === 0; + return crypto.subtle.timingSafeEqual(aBytes, bBytes); } diff --git a/kiloclaw/src/test-setup.ts b/kiloclaw/src/test-setup.ts new file mode 100644 index 000000000..80ab269f1 --- /dev/null +++ b/kiloclaw/src/test-setup.ts @@ -0,0 +1,17 @@ +import { timingSafeEqual } from 'node:crypto'; + +// Polyfill crypto.subtle.timingSafeEqual for Vitest (Node environment). +// This API is available natively in Cloudflare Workers but not in Node.js. +if (!crypto.subtle.timingSafeEqual) { + Object.defineProperty(crypto.subtle, 'timingSafeEqual', { + value(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean { + const bufA = ArrayBuffer.isView(a) + ? Buffer.from(a.buffer, a.byteOffset, a.byteLength) + : Buffer.from(a); + const bufB = ArrayBuffer.isView(b) + ? Buffer.from(b.buffer, b.byteOffset, b.byteLength) + : Buffer.from(b); + return timingSafeEqual(bufA, bufB); + }, + }); +} diff --git a/kiloclaw/vitest.config.ts b/kiloclaw/vitest.config.ts index bfef76e28..58e6c65a9 100644 --- a/kiloclaw/vitest.config.ts +++ b/kiloclaw/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ name: 'unit', globals: true, environment: 'node', + setupFiles: ['src/test-setup.ts'], include: ['src/**/*.test.ts'], coverage: { provider: 'v8',