diff --git a/apps/mcp-server/src/mcp/sse-auth.guard.spec.ts b/apps/mcp-server/src/mcp/sse-auth.guard.spec.ts index 4f7f068..a4a202c 100644 --- a/apps/mcp-server/src/mcp/sse-auth.guard.spec.ts +++ b/apps/mcp-server/src/mcp/sse-auth.guard.spec.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createHmac } from 'crypto'; import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { SseAuthGuard } from './sse-auth.guard'; @@ -92,5 +93,22 @@ describe('SseAuthGuard', () => { expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); }); + + it('should reject tokens of different lengths without timing leak', () => { + const context = createMockContext('Bearer short'); + + expect(() => guard.canActivate(context)).toThrow(UnauthorizedException); + }); + + it('should use HMAC-based comparison (no length early-return)', () => { + // Verify HMAC produces fixed-length digests regardless of input length + const key = Buffer.from('codingbuddy-sse-auth'); + const expectedHash = createHmac('sha256', key).update('my-secret-token').digest(); + const providedHash = createHmac('sha256', key).update('wrong').digest(); + + // Both hashes should be same length (32 bytes) regardless of input length + expect(expectedHash.length).toBe(providedHash.length); + expect(expectedHash.length).toBe(32); + }); }); }); diff --git a/apps/mcp-server/src/mcp/sse-auth.guard.ts b/apps/mcp-server/src/mcp/sse-auth.guard.ts index bbc7f23..a249f55 100644 --- a/apps/mcp-server/src/mcp/sse-auth.guard.ts +++ b/apps/mcp-server/src/mcp/sse-auth.guard.ts @@ -6,7 +6,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import { Request } from 'express'; -import { timingSafeEqual } from 'crypto'; +import { createHmac, timingSafeEqual } from 'crypto'; /** * Guard for SSE endpoints that validates Bearer token against MCP_SSE_TOKEN env var. @@ -53,17 +53,13 @@ export class SseAuthGuard implements CanActivate { } /** - * Timing-safe token comparison to prevent timing attacks. - * Handles tokens of different lengths safely. + * Timing-safe token comparison using HMAC to prevent timing attacks. + * HMAC produces fixed-length digests, eliminating length-based timing leaks. */ private tokensMatch(expected: string, provided: string): boolean { - const expectedBuf = Buffer.from(expected, 'utf-8'); - const providedBuf = Buffer.from(provided, 'utf-8'); - - if (expectedBuf.length !== providedBuf.length) { - return false; - } - - return timingSafeEqual(expectedBuf, providedBuf); + const key = Buffer.from('codingbuddy-sse-auth'); + const expectedHash = createHmac('sha256', key).update(expected).digest(); + const providedHash = createHmac('sha256', key).update(provided).digest(); + return timingSafeEqual(expectedHash, providedHash); } }