From 9c1b6ff01a5a40a2223d6f69a2d18235d98be82b Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:11:49 -0500 Subject: [PATCH] Privacy.ts: Additional safety for debug logs Co-authored-by: Scott Nonnenberg --- ts/test-node/util/privacy_test.ts | 106 ++++++++++++++++++++++++++++++ ts/util/privacy.ts | 35 +++++++--- 2 files changed, 132 insertions(+), 9 deletions(-) diff --git a/ts/test-node/util/privacy_test.ts b/ts/test-node/util/privacy_test.ts index b8a36e6f09..a20cb4b0b4 100644 --- a/ts/test-node/util/privacy_test.ts +++ b/ts/test-node/util/privacy_test.ts @@ -9,6 +9,110 @@ import { APP_ROOT_PATH } from '../../util/privacy'; Privacy.addSensitivePath('sensitive-path'); describe('Privacy', () => { + describe('redactCardNumbers', () => { + it('should redact anything that looks like a credit card', () => { + const text = + 'This is a log line with a card number 1234-1234-1234\n' + + 'and another one 1234 1234 1234 1234 123'; + + const actual = Privacy.redactCardNumbers(text); + const expected = + 'This is a log line with a card number [REDACTED]\n' + + 'and another one [REDACTED]'; + assert.equal(actual, expected); + }); + + it('should redact weird credit card numbers', () => { + const text = + '12341234123\n' + + '123412341234\n' + + '1234123412341\n' + + '12341234123412\n' + + '123412341234123\n' + + '1234123412341234\n' + + '12341234123412341\n' + + '123412341234123412\n' + + '1234123412341234123\n' + + '12341234123412341234\n' + + '1-2-3-4-1-2-3-4-1-2-3\n' + + '1-2-3-4-1-2-3-4-1-2-3-4\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1-2\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1-2-3\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1-2-3-4\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1-2-3-4-1\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1-2-3-4-1-2\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1-2-3-4-1-2-3\n' + + '1-2-3-4-1-2-3-4-1-2-3-4-1-2-3-4-1-2-3-4\n' + + '1 2 3 4 1 2 3 4 1 2 3\n' + + '1 2 3 4 1 2 3 4 1 2 3 4\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2 3\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4\n' + + '1 2 3 a 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4\n' + + '1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 a 2 3 4\n' + + ''; + + const actual = Privacy.redactCardNumbers(text); + const expected = + '12341234123\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]4\n' + + '1-2-3-4-1-2-3-4-1-2-3\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]-4\n' + + '1 2 3 4 1 2 3 4 1 2 3\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED]\n' + + '[REDACTED] 4\n' + + '1 2 3 a [REDACTED]\n' + + '[REDACTED] a 2 3 4\n' + + ''; + assert.equal(actual, expected); + }); + + it('should not redact things that are close to credit card numbers', () => { + const text = ` + 12--3412341234 + 1234123 412341234 + 1e23412341234 + `; + + const actual = Privacy.redactCardNumbers(text); + const expected = ` + 12--3412341234 + 1234123 412341234 + 1e23412341234 + `; + assert.equal(actual, expected); + }); + }); + describe('redactPhoneNumbers', () => { it('should redact all phone numbers', () => { const text = @@ -143,6 +247,7 @@ describe('Privacy', () => { 'phone2 +13334445566 lorem\n' + 'group2 group(abcdefghij) doloret\n' + 'path3 sensitive-path/attachment.noindex\n' + + 'cc 1234 1234 1234 1234 and another 1234123412341234\n' + 'attachment://v2/ab/abcde?key=specialkey\n'; const actual = Privacy.redactAll(text); @@ -155,6 +260,7 @@ describe('Privacy', () => { 'phone2 +[REDACTED]566 lorem\n' + 'group2 group([REDACTED]hij) doloret\n' + 'path3 [REDACTED]/attachment.noindex\n' + + 'cc [REDACTED] and another [REDACTED]\n' + 'attachment://v2/ab/abcde?key=[REDACTED]\n'; assert.equal(actual, expected); }); diff --git a/ts/util/privacy.ts b/ts/util/privacy.ts index 3aacb2713e..e62cfe299c 100644 --- a/ts/util/privacy.ts +++ b/ts/util/privacy.ts @@ -24,6 +24,7 @@ const CALL_LINK_ROOT_KEY_PATTERN = /([A-Z]{4})-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}/gi; const ATTACHMENT_URL_KEY_PATTERN = /(attachment:\/\/[^\s]+key=)([^\s]+)/gi; const REDACTION_PLACEHOLDER = '[REDACTED]'; +const CARD_NUMBER_PATTERN = /(\d[- ]?){11,18}\d/g; export type RedactFunction = (value: string) => string; @@ -110,6 +111,17 @@ export const _pathToRegExp = (filePath: string): RegExp | undefined => { }; // Public API + +// As part of supporting credit card donations, we also want to include an extra safety +// layer to prevent CC #s from being logged even if a bug occurs in the payment interface. +export const redactCardNumbers = (text: string): string => { + if (!isString(text)) { + throw new TypeError("'text' must be a string"); + } + + return text.replace(CARD_NUMBER_PATTERN, '[REDACTED]'); +}; + export const redactPhoneNumbers = (text: string): string => { if (!isString(text)) { throw new TypeError("'text' must be a string"); @@ -207,14 +219,19 @@ export const addSensitivePath = (filePath: string): void => { addSensitivePath(APP_ROOT_PATH); -export const redactAll: RedactFunction = compose( - (text: string) => redactSensitivePaths(text), - redactGroupIds, - redactPhoneNumbers, - redactUuids, - redactCallLinkRoomIds, - redactCallLinkRootKeys, - redactAttachmentUrlKeys -); +export const redactAll: RedactFunction = text => { + let result = text; + + result = redactAttachmentUrlKeys(result); + result = redactCallLinkRoomIds(result); + result = redactCallLinkRootKeys(result); + result = redactCardNumbers(result); + result = redactGroupIds(result); + result = redactPhoneNumbers(result); + result = redactSensitivePaths(result); + result = redactUuids(result); + + return result; +}; const removeNewlines: RedactFunction = text => text.replace(/\r?\n|\r/g, '');