From 7f4b5be848bed99d69a03ab6f1f1c968a09d4bc8 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 19 Jul 2026 07:52:11 +0200 Subject: [PATCH] fix(logging): close remaining credential leaks --- .../src/app/services/electron.service.spec.ts | 20 +++++++++++++++++++ apps/web/src/app/services/electron.service.ts | 13 +++++++----- .../src/lib/redact-sensitive-data.spec.ts | 13 ++++++++++++ .../logging/src/lib/redact-sensitive-data.ts | 16 ++++++--------- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/services/electron.service.spec.ts b/apps/web/src/app/services/electron.service.spec.ts index ad0c5015b..e7000a6e4 100644 --- a/apps/web/src/app/services/electron.service.spec.ts +++ b/apps/web/src/app/services/electron.service.spec.ts @@ -16,6 +16,7 @@ describe('ElectronService', () => { const session = { id: 'session-1' }; let electronBridge: { fetchPlaylistByUrl: jest.Mock; + onPlayerError: jest.Mock; openInMpv: jest.Mock; openInVlc: jest.Mock; }; @@ -24,9 +25,11 @@ describe('ElectronService', () => { beforeEach(() => { jest.spyOn(console, 'log').mockImplementation(() => undefined); + jest.spyOn(console, 'error').mockImplementation(() => undefined); electronBridge = { fetchPlaylistByUrl: jest.fn(), + onPlayerError: jest.fn(), openInMpv: jest.fn().mockResolvedValue(session), openInVlc: jest.fn().mockResolvedValue(session), }; @@ -99,6 +102,23 @@ describe('ElectronService', () => { expect(electronBridge.fetchPlaylistByUrl).not.toHaveBeenCalled(); }); + it('redacts credentials from backend player errors before logging', () => { + const secret = 'player-error-token-secret'; + const listener = electronBridge.onPlayerError.mock.calls[0][0]; + + listener({ + player: 'MPV', + error: 'Playback failed', + originalError: `Request failed: token=${secret}&channel=news`, + }); + + const output = JSON.stringify( + (console.error as jest.Mock).mock.calls + ); + expect(output).not.toContain(secret); + expect(output).toContain('channel=news'); + }); + it('shows the trust-host action for Electron-wrapped security errors', async () => { const securityPayload = { code: ELECTRON_BRIDGE_SECURITY_ERROR_CODES.InvalidTlsCertificate, diff --git a/apps/web/src/app/services/electron.service.ts b/apps/web/src/app/services/electron.service.ts index 88e4ec625..7ffdaf6f5 100644 --- a/apps/web/src/app/services/electron.service.ts +++ b/apps/web/src/app/services/electron.service.ts @@ -80,7 +80,10 @@ export class ElectronService extends DataService { error: string; originalError: string; }) => { - console.error(`${data.player} Error:`, data.originalError); + this.logger.error( + `${data.player} Error:`, + data.originalError + ); this.snackBar.open( `${data.player} Error: ${data.error}`, 'Close', @@ -181,7 +184,7 @@ export class ElectronService extends DataService { duration: 5000, } ); - console.error('MPV launch error:', error); + this.logger.error('MPV launch error:', error); throw error; } } @@ -210,7 +213,7 @@ export class ElectronService extends DataService { duration: 5000, } ); - console.error('VLC launch error:', error); + this.logger.error('VLC launch error:', error); throw error; } } @@ -361,7 +364,7 @@ export class ElectronService extends DataService { data.title ); } else { - console.error( + this.logger.error( 'Either url or filePath must be provided, but not both.' ); return; @@ -386,7 +389,7 @@ export class ElectronService extends DataService { { duration: 2000 } ); } catch (error: unknown) { - console.error('Playlist refresh error:', error); + this.logger.error('Playlist refresh error:', error); if ( data.url && this.handlePlaylistSecurityError(error, () => { diff --git a/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts b/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts index 2c3eab998..75d6efcb6 100644 --- a/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts +++ b/libs/shared/logging/src/lib/redact-sensitive-data.spec.ts @@ -51,10 +51,19 @@ describe('redactSensitiveData', () => { it('redacts Stalker identity credentials while retaining request diagnostics', () => { const identitySecrets = { sn: 'stalker-sn-secret', + serialNumber: 'stalker-serial-number-secret', device_id: 'stalker-device-id-secret', + deviceId1: 'stalker-device-id1-secret', device_id2: 'stalker-device-id2-secret', signature: 'stalker-signature-secret', + signature1: 'stalker-signature1-secret', signature2: 'stalker-signature2-secret', + stalkerSerialNumber: 'playlist-stalker-serial-number-secret', + stalkerDeviceId1: 'playlist-stalker-device-id1-secret', + stalkerDeviceId2: 'playlist-stalker-device-id2-secret', + stalkerSignature1: 'playlist-stalker-signature1-secret', + stalkerSignature2: 'playlist-stalker-signature2-secret', + prehash: 'stalker-prehash-secret', }; const result = redactSensitiveData({ @@ -141,10 +150,12 @@ describe('redactSensitiveData', () => { const authorization = 'diagnostic-authorization-secret'; const authorizationAssignment = 'diagnostic-authorization-assignment-secret'; + const stalkerDeviceId = 'diagnostic-stalker-device-id-secret'; const diagnostic = [ `Request failed: token=${token}&action=get_profile`, `Upstream response Authorization: Bearer ${authorization}; status=401`, `Retrying with authorization=Bearer ${authorizationAssignment}, attempt=2`, + `Identity rejected: stalkerDeviceId1: ${stalkerDeviceId}; action=handshake`, ]; const output = serialized(redactSensitiveData(diagnostic)); @@ -152,9 +163,11 @@ describe('redactSensitiveData', () => { expect(output).not.toContain(token); expect(output).not.toContain(authorization); expect(output).not.toContain(authorizationAssignment); + expect(output).not.toContain(stalkerDeviceId); expect(output).toContain('get_profile'); expect(output).toContain('status=401'); expect(output).toContain('attempt=2'); + expect(output).toContain('action=handshake'); }); it('does not repeat a redacted Error message secret in its stack', () => { diff --git a/libs/shared/logging/src/lib/redact-sensitive-data.ts b/libs/shared/logging/src/lib/redact-sensitive-data.ts index 77e6a0e7b..cc42a4db2 100644 --- a/libs/shared/logging/src/lib/redact-sensitive-data.ts +++ b/libs/shared/logging/src/lib/redact-sensitive-data.ts @@ -33,15 +33,11 @@ const SENSITIVE_KEY_NAMES = new Set([ ]); const SENSITIVE_KEY_SUFFIXES = [ - 'apikey', - 'authorization', - 'cookie', - 'macaddress', - 'passwd', - 'password', - 'secret', - 'token', - 'username', + 'apikey', 'authorization', 'cookie', + 'deviceid', 'deviceid1', 'deviceid2', + 'macaddress', 'passwd', 'password', 'prehash', + 'serialnumber', 'signature', 'signature1', 'signature2', + 'secret', 'token', 'username', ]; const XTREAM_CREDENTIAL_PATH_SEGMENTS = new Set([ @@ -185,7 +181,7 @@ function redactEmbeddedSensitivePairs(value: string): string { : match ); return redactedAssignments.replace( - /\b([a-z0-9_.-]*(?:api[-_.]?key|auth(?:orization)?|cookie|credentials|device[-_.]?id2?|login|mac(?:[-_.]?address)?|passwd|password|pwd|secret|set[-_.]?cookie|signature2?|sn|token|username))(\s*:\s*)(?:Bearer\s+)?[^;,\r\n]+/giu, + /\b([a-z0-9_.-]*(?:api[-_.]?key|auth(?:orization)?|cookie|credentials|device[-_.]?id[12]?|login|mac(?:[-_.]?address)?|passwd|password|prehash|pwd|secret|serial[-_.]?number|set[-_.]?cookie|signature[12]?|sn|token|username))(\s*:\s*)(?:Bearer\s+)?[^;,\r\n]+/giu, (_match, key: string, separator: string) => `${key}${separator}${REDACTED_VALUE}` );