From 8c23d3fd19318b79dbd5e0eb3bebde9c02e879e4 Mon Sep 17 00:00:00 2001 From: akordavid373 Date: Sat, 27 Jun 2026 12:50:21 +0100 Subject: [PATCH] security: replace Math.random with crypto.randomUUID in structuredLogger (#443) - Replace Math.random-based sessionId generation with crypto.randomUUID() - Replace Math.random-based error ID generation with crypto.randomUUID() - Add structuredLogger tests asserting crypto.randomUUID usage - Add crypto.randomUUID polyfill to jest.setup.js for jsdom environment - Remove unpredictable Math.random usage from security-sensitive ID generation --- jest.setup.js | 13 ++++ src/utils/__tests__/structuredLogger.test.ts | 77 ++++++++++++++++++++ src/utils/structuredLogger.ts | 4 +- 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/utils/__tests__/structuredLogger.test.ts diff --git a/jest.setup.js b/jest.setup.js index 53202842..3ea5e2ee 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -137,3 +137,16 @@ const sessionStorageMock = { clear: jest.fn(), } global.sessionStorage = sessionStorageMock + +// Polyfill crypto.randomUUID for jsdom (Node.js <19) +if (typeof globalThis.crypto !== 'undefined' && !globalThis.crypto.randomUUID) { + let counter = BigInt(0); + globalThis.crypto.randomUUID = function randomUUID() { + counter++; + const hex = (counter + BigInt(Date.now()) * BigInt(100000)).toString(16); + return hex.slice(0, 36).replace( + /^(.{8})(.{4})(.{4})(.{4})(.{12})$/, + '$1-$2-4$3-a$4-$5' + ); + } +} diff --git a/src/utils/__tests__/structuredLogger.test.ts b/src/utils/__tests__/structuredLogger.test.ts new file mode 100644 index 00000000..532c5f32 --- /dev/null +++ b/src/utils/__tests__/structuredLogger.test.ts @@ -0,0 +1,77 @@ +import { structuredLogger, createStructuredLogger } from '../structuredLogger'; +import { errorReporting } from '../errorReporting'; + +jest.mock('../logger', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + getCorrelationId: jest.fn(() => 'test-correlation-id'), + }, + createLogger: jest.fn(), + configureLogger: jest.fn(), + getLoggerConfig: jest.fn(), + createRequestLogger: jest.fn(), + replaceConsole: jest.fn(), + createPerformanceLogger: jest.fn(), + LogLevel: { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 }, +})); + +jest.mock('../errorReporting', () => ({ + errorReporting: { + reportError: jest.fn(), + }, +})); + +describe('structuredLogger', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('sessionId generation', () => { + it('should use crypto.randomUUID instead of Math.random', () => { + const spy = jest.spyOn(globalThis.crypto, 'randomUUID'); + const customLogger = createStructuredLogger({ enableErrorTracking: false, enableRemote: false }); + + expect(spy).toHaveBeenCalled(); + expect(customLogger['sessionId']).toMatch(/^sess_\d+_[a-f0-9]+$/); + + spy.mockRestore(); + customLogger.destroy(); + }); + + it('should generate session IDs from crypto.randomUUID output', () => { + const customLogger = createStructuredLogger({ enableErrorTracking: false, enableRemote: false }); + expect(customLogger['sessionId']).toMatch(/^sess_\d+_[a-f0-9]{7}$/); + customLogger.destroy(); + }); + }); + + describe('error id generation', () => { + it('should use crypto.randomUUID for error IDs', () => { + const spy = jest.spyOn(globalThis.crypto, 'randomUUID'); + const customLogger = createStructuredLogger({ enableErrorTracking: true, enableRemote: false }); + + customLogger.error('test error', new Error('test')); + customLogger.destroy(); + + expect(errorReporting.reportError).toHaveBeenCalled(); + const reportedError = (errorReporting.reportError as jest.Mock).mock.calls[0][0]; + expect(reportedError.id).toMatch(/^error_\d+_[a-f0-9]+$/); + + spy.mockRestore(); + }); + }); + + describe('non-predictability', () => { + it('should generate IDs that are not predictable Math.random output', () => { + const customLogger = createStructuredLogger({ enableErrorTracking: false, enableRemote: false }); + + expect(customLogger['sessionId']).not.toContain('Math'); + expect(customLogger['sessionId']).toMatch(/^sess_\d+_[a-f0-9]+$/); + + customLogger.destroy(); + }); + }); +}); diff --git a/src/utils/structuredLogger.ts b/src/utils/structuredLogger.ts index 0e48cd9f..53957c42 100644 --- a/src/utils/structuredLogger.ts +++ b/src/utils/structuredLogger.ts @@ -83,7 +83,7 @@ class StructuredLogger { flushInterval: 5000, ...config, }; - this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + this.sessionId = `sess_${Date.now()}_${crypto.randomUUID().replace(/-/g, '').substring(0, 7)}`; this.startFlushTimer(); } @@ -135,7 +135,7 @@ class StructuredLogger { private reportError(entry: StructuredLogEntry): void { if (!entry.error) return; const appError: AppError = { - id: `error_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: `error_${Date.now()}_${globalThis.crypto.randomUUID().split('-').join('').substring(0, 7)}`, message: entry.error.message, category: entry.category ?? ErrorCategory.UI, severity: entry.severity ?? ErrorSeverity.MEDIUM,