fix(logging): redact Stalker session references

This commit is contained in:
4gray committed 2026-07-27 10:45:26 +02:00
1 parent b76e65c2bf
commit 00c85beb70
3 files changed
+188 -75

No files matched your search

@@ -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);
}
@@ -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');
});
});
@@ -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}`
);
}