mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(logging): redact Stalker session references
This commit is contained in:
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}`
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user