From c77b4efed4107ab7a3fd756c1c6747567af84281 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:20:22 +0000 Subject: [PATCH] Fix timing attack in token authentication The previous implementation of token authentication in `src/cli/commands/serve.ts` short-circuited constant-time comparison if the lengths of the provided token and expected token did not match. This allowed attackers to infer the length of a token via a timing side channel. This fix hashes both the provided and expected tokens to a fixed length using SHA-256 prior to calling `crypto.timingSafeEqual`, ensuring a true constant-time comparison. --- .jules/sentinel.md | 4 ++++ src/cli/commands/serve.ts | 7 +++---- tests/integration/prompt_templates.test.ts | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..c8db18a4 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-14 - Fix Timing Attack in Authentication +**Vulnerability:** Fast-path short-circuiting in token validation using buffer length comparisons before calling `crypto.timingSafeEqual` introduces a timing attack vulnerability. +**Learning:** Evaluating the lengths of tokens before performing a constant-time comparison leaks the length of the secret auth token. +**Prevention:** Always hash both the expected and provided secrets to a fixed length (e.g., using `crypto.createHash('sha256')`) before comparison to ensure true constant-time evaluation regardless of input length. diff --git a/src/cli/commands/serve.ts b/src/cli/commands/serve.ts index 2c37293e..09448b63 100644 --- a/src/cli/commands/serve.ts +++ b/src/cli/commands/serve.ts @@ -349,10 +349,9 @@ export async function handleServeCommand(_options: unknown, command: Command) { const tokenBuffer = Buffer.from(token); for (const authToken of authTokens) { const authTokenBuffer = Buffer.from(authToken); - if ( - tokenBuffer.length === authTokenBuffer.length && - crypto.timingSafeEqual(tokenBuffer, authTokenBuffer) - ) { + const tokenHash = crypto.createHash('sha256').update(tokenBuffer).digest(); + const expectedHash = crypto.createHash('sha256').update(authTokenBuffer).digest(); + if (crypto.timingSafeEqual(tokenHash, expectedHash)) { isAuthenticated = true; break; } diff --git a/tests/integration/prompt_templates.test.ts b/tests/integration/prompt_templates.test.ts index 21905259..f1360d78 100644 --- a/tests/integration/prompt_templates.test.ts +++ b/tests/integration/prompt_templates.test.ts @@ -36,7 +36,9 @@ describe('Prompt templates', () => { expect(planSystem).toContain('You are SalmonLoop.'); expect(patchSystem).toContain('You are PATCH, a phase-native diff compiler.'); - expect(autopilotSystem).toContain('You are a senior software engineer running in "autopilot" mode.'); + expect(autopilotSystem).toContain( + 'You are a senior software engineer running in "autopilot" mode.', + ); expect(answerSystem).toContain('You are a coding assistant in "answer" mode.'); expect(researchSystem).toContain('You are a research assistant.'); });