From 00c85beb706684fb37db324764dc35147d207580 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 27 Jul 2026 10:45:26 +0200 Subject: [PATCH] fix(logging): redact Stalker session references --- .../logging/src/lib/redact-sensitive-data.ts | 92 ++++--------------- .../src/lib/redact-stalker-session.spec.ts | 79 ++++++++++++++++ libs/shared/logging/src/lib/sensitive-key.ts | 92 +++++++++++++++++++ 3 files changed, 188 insertions(+), 75 deletions(-) create mode 100644 libs/shared/logging/src/lib/redact-stalker-session.spec.ts create mode 100644 libs/shared/logging/src/lib/sensitive-key.ts diff --git a/libs/shared/logging/src/lib/redact-sensitive-data.ts b/libs/shared/logging/src/lib/redact-sensitive-data.ts index cc42a4db2..a5f660a0b 100644 --- a/libs/shared/logging/src/lib/redact-sensitive-data.ts +++ b/libs/shared/logging/src/lib/redact-sensitive-data.ts @@ -1,3 +1,5 @@ +import { isSensitiveKey, redactEmbeddedSensitivePairs } from './sensitive-key'; + export const REDACTED_VALUE = '[Redacted]'; const CIRCULAR_VALUE = '[Circular]'; @@ -7,39 +9,6 @@ const DEFAULT_MAX_ARRAY_ITEMS = 50; const DEFAULT_MAX_OBJECT_KEYS = 50; const DEFAULT_MAX_STRING_LENGTH = 2_000; -const SENSITIVE_KEY_NAMES = new Set([ - 'apikey', - 'auth', - 'authorization', - 'cookie', - 'credentials', - 'deviceid', - 'deviceid2', - 'login', - 'mac', - 'macaddress', - 'mpvplayerarguments', - 'passwd', - 'password', - 'pwd', - 'secret', - 'setcookie', - 'signature', - 'signature2', - 'sn', - 'token', - 'username', - 'vlcplayerarguments', -]); - -const SENSITIVE_KEY_SUFFIXES = [ - 'apikey', 'authorization', 'cookie', - 'deviceid', 'deviceid1', 'deviceid2', - 'macaddress', 'passwd', 'password', 'prehash', - 'serialnumber', 'signature', 'signature1', 'signature2', - 'secret', 'token', 'username', -]; - const XTREAM_CREDENTIAL_PATH_SEGMENTS = new Set([ 'live', 'movie', @@ -61,18 +30,6 @@ interface ResolvedRedactionOptions { maxStringLength: number; } -function normalizeKey(key: string): string { - return key.toLowerCase().replace(/[^a-z0-9]/g, ''); -} - -function isSensitiveKey(key: string): boolean { - const normalized = normalizeKey(key); - return ( - SENSITIVE_KEY_NAMES.has(normalized) || - SENSITIVE_KEY_SUFFIXES.some((suffix) => normalized.endsWith(suffix)) - ); -} - function resolveOptions(options: RedactionOptions): ResolvedRedactionOptions { return { maxDepth: Math.max(0, options.maxDepth ?? DEFAULT_MAX_DEPTH), @@ -160,31 +117,13 @@ function redactUrlStrings( value: string, sanitizeValue: (value: string) => string ): string { - return value.replace( - /[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/giu, - (candidate) => { - try { - return redactUrl(new URL(candidate), sanitizeValue); - } catch { - return candidate; - } + return value.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/giu, (candidate) => { + try { + return redactUrl(new URL(candidate), sanitizeValue); + } catch { + return candidate; } - ); -} - -function redactEmbeddedSensitivePairs(value: string): string { - const redactedAssignments = value.replace( - /\b([a-z][a-z0-9_.-]*)(\s*=\s*)((?:Bearer\s+)?[^&\s,;]+)/giu, - (match, key: string, separator: string) => - isSensitiveKey(key) - ? `${key}${separator}${REDACTED_VALUE}` - : match - ); - return redactedAssignments.replace( - /\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}` - ); + }); } function looksLikeSearchParams(value: string): boolean { @@ -237,7 +176,10 @@ export function redactSensitiveData( ); } - const redactedText = redactEmbeddedSensitivePairs(input); + const redactedText = redactEmbeddedSensitivePairs( + input, + REDACTED_VALUE + ); return truncateString( redactUrlStrings(redactedText, (entry) => visitString(entry, depth + 1) @@ -286,9 +228,10 @@ export function redactSensitiveData( if (error.stack) { const [, ...stackFrames] = error.stack.split('\n'); output['stack'] = visitString( - [`${output['name']}: ${output['message']}`, ...stackFrames].join( - '\n' - ), + [ + `${output['name']}: ${output['message']}`, + ...stackFrames, + ].join('\n'), depth + 1 ); } @@ -370,8 +313,7 @@ export function redactSensitiveData( const stringKey = String(key); const redactedKey = visitString(stringKey, depth + 1); entries[redactedKey] = - isSensitiveKey(stringKey) && - !/[/:?=&]/u.test(stringKey) + isSensitiveKey(stringKey) && !/[/:?=&]/u.test(stringKey) ? REDACTED_VALUE : visit(entry, depth + 1); } diff --git a/libs/shared/logging/src/lib/redact-stalker-session.spec.ts b/libs/shared/logging/src/lib/redact-stalker-session.spec.ts new file mode 100644 index 000000000..3f1e29b9d --- /dev/null +++ b/libs/shared/logging/src/lib/redact-stalker-session.spec.ts @@ -0,0 +1,79 @@ +import { redactSensitiveData } from './redact-sensitive-data'; + +function serialized(value: unknown): string { + return JSON.stringify(value); +} + +describe('Stalker session redaction', () => { + it('redacts every main-owned session reference and authentication value', () => { + const secrets = { + mac: '02:00:00:00:00:01', + serial: 'EXPLICIT-SERIAL', + device: 'EXPLICIT-DEVICE', + signature: 'EXPLICIT-SIGNATURE', + challengeRef: 'opaque-challenge-secret', + leaseRef: 'opaque-lease-secret', + attemptRef: 'opaque-attempt-secret', + playbackContextRef: 'opaque-playback-secret', + sessionKey: 'internal-session-secret', + random: 'handshake-random-secret', + cookie: 'session=cookie-secret', + token: 'bearer-token-secret', + }; + const value = { + descriptor: { + sourceUrl: 'https://portal.example/c/', + macAddress: secrets.mac, + identityOverrides: { + serialNumber: secrets.serial, + deviceId1: secrets.device, + signature1: secrets.signature, + }, + }, + outcome: { + kind: 'ready', + challengeRef: secrets.challengeRef, + leaseRef: secrets.leaseRef, + attemptRef: secrets.attemptRef, + playbackContextRef: secrets.playbackContextRef, + }, + diagnostic: { + sessionKey: secrets.sessionKey, + handshakeRandom: secrets.random, + headers: { + Authorization: `Bearer ${secrets.token}`, + Cookie: secrets.cookie, + }, + }, + }; + + const output = serialized(redactSensitiveData(value)); + + for (const secret of Object.values(secrets)) { + expect(output).not.toContain(secret); + } + expect(output).toContain('https://portal.example/c/'); + expect(output).toContain('"kind":"ready"'); + }); + + it('redacts refs and identity values in query strings and diagnostic text', () => { + const challengeRef = 'query-challenge-secret'; + const leaseRef = 'query-lease-secret'; + const random = 'query-random-secret'; + const device = 'query-device-secret'; + const url = new URL( + `https://portal.example/api?challengeRef=${challengeRef}&lease_ref=${leaseRef}&random=${random}&device_id=${device}&action=get_profile` + ); + const text = + `challengeRef: ${challengeRef}; leaseRef=${leaseRef}; ` + + `handshakeRandom: ${random}; action=get_profile`; + + const output = serialized(redactSensitiveData({ url, text })); + + expect(output).not.toContain(challengeRef); + expect(output).not.toContain(leaseRef); + expect(output).not.toContain(random); + expect(output).not.toContain(device); + expect(output).toContain('get_profile'); + }); +}); diff --git a/libs/shared/logging/src/lib/sensitive-key.ts b/libs/shared/logging/src/lib/sensitive-key.ts new file mode 100644 index 000000000..c01444a9f --- /dev/null +++ b/libs/shared/logging/src/lib/sensitive-key.ts @@ -0,0 +1,92 @@ +const SENSITIVE_KEY_NAMES = new Set([ + 'apikey', + 'attemptref', + 'auth', + 'authorization', + 'challengeref', + 'cookie', + 'contextref', + 'credentials', + 'deviceid', + 'deviceid2', + 'login', + 'leaseref', + 'mac', + 'macaddress', + 'mpvplayerarguments', + 'passwd', + 'password', + 'playbackcontextref', + 'principal', + 'principalkey', + 'pwd', + 'random', + 'secret', + 'sessionkey', + 'setcookie', + 'signature', + 'signature2', + 'sn', + 'token', + 'username', + 'vlcplayerarguments', +]); + +const SENSITIVE_KEY_SUFFIXES = [ + 'apikey', + 'attemptref', + 'authorization', + 'challengeref', + 'cookie', + 'contextref', + 'deviceid', + 'deviceid1', + 'deviceid2', + 'handshakerandom', + 'identityrevision', + 'leaseref', + 'macaddress', + 'passwd', + 'password', + 'playbackcontextref', + 'prehash', + 'principal', + 'principalkey', + 'random', + 'serialnumber', + 'signature', + 'signature1', + 'signature2', + 'secret', + 'sessionkey', + 'token', + 'username', +]; + +function normalizeKey(key: string): string { + return key.toLowerCase().replace(/[^a-z0-9]/g, ''); +} + +export function isSensitiveKey(key: string): boolean { + const normalized = normalizeKey(key); + return ( + SENSITIVE_KEY_NAMES.has(normalized) || + SENSITIVE_KEY_SUFFIXES.some((suffix) => normalized.endsWith(suffix)) + ); +} + +export function redactEmbeddedSensitivePairs( + value: string, + redactedValue: string +): string { + const redactedAssignments = value.replace( + /\b([a-z][a-z0-9_.-]*)(\s*=\s*)((?:Bearer\s+)?[^&\s,;]+)/giu, + (match, key: string, separator: string) => + isSensitiveKey(key) ? `${key}${separator}${redactedValue}` : match + ); + return redactedAssignments.replace( + /\b([a-z0-9_.-]*(?:api[-_.]?key|attempt[-_.]?ref|auth(?:orization)?|challenge[-_.]?ref|context[-_.]?ref|cookie|credentials|device[-_.]?id[12]?|handshake[-_.]?random|lease[-_.]?ref|login|mac(?:[-_.]?address)?|passwd|password|playback[-_.]?context[-_.]?ref|prehash|principal(?:[-_.]?key)?|pwd|random|secret|serial[-_.]?number|session[-_.]?key|set[-_.]?cookie|signature[12]?|sn|token|username))(\s*:\s*)(?:Bearer\s+)?[^;,\r\n]+/giu, + (_match, key: string, separator: string) => + `${key}${separator}${redactedValue}` + ); +}