diff --git a/workers/grouper/src/data-filter.ts b/workers/grouper/src/data-filter.ts index 3571a1c6..7e00038c 100644 --- a/workers/grouper/src/data-filter.ts +++ b/workers/grouper/src/data-filter.ts @@ -36,17 +36,71 @@ export default class DataFilter { private filteredValuePlaceholder = '[filtered]'; /** - * Possibly sensitive keys + * Possibly sensitive keys (lowercase; keys are compared via key.toLowerCase()) */ private possiblySensitiveDataKeys = new Set([ - 'pan', - 'secret', - 'credentials', - 'card[number]', - 'password', + /** + * Authorization and sessions + */ 'auth', + 'authorization', 'access_token', 'accesstoken', + 'token', + 'jwt', + 'session', + 'sessionid', + 'session_id', + /** + * API keys and secure tokens + */ + 'api_key', + 'apikey', + 'x-api-key', + 'x-auth-token', + 'bearer', + 'client_secret', + 'secret', + 'credentials', + /** + * Passwords + */ + 'password', + 'passwd', + 'mysql_pwd', + 'oldpassword', + 'old-password', + 'old_password', + 'newpassword', + 'new-password', + 'new_password', + /** + * Encryption keys + */ + 'private_key', + 'ssh_key', + /** + * Payments data + */ + 'card', + 'cardnumber', + 'card[number]', + 'creditcard', + 'credit_card', + 'pan', + 'pin', + 'security_code', + 'stripetoken', + 'cloudpayments_public_id', + 'cloudpayments_secret', + /** + * Config and connections + */ + 'dsn', + /** + * Personal data + */ + 'ssn', ]); /** @@ -127,7 +181,9 @@ export default class DataFilter { */ const clean = value.replace(/\D/g, ''); - // Reset last index to 0 + /** + * Reset last index to 0 + */ this.bankCardRegex.lastIndex = 0; if (!this.bankCardRegex.test(clean)) { return value; diff --git a/workers/grouper/tests/data-filter.test.ts b/workers/grouper/tests/data-filter.test.ts index 28ff2979..4cb98807 100644 --- a/workers/grouper/tests/data-filter.test.ts +++ b/workers/grouper/tests/data-filter.test.ts @@ -28,20 +28,57 @@ function generateEvent({ context, addons }: {context?: Json, addons?: EventAddon } /** - * Example of object with sensitive information + * Example of object with sensitive information. + * Keys intentionally use snake_case/kebab-case to match data-filter list. */ +/* eslint-disable @typescript-eslint/naming-convention */ const sensitiveDataMock = { pan: '5500 0000 0000 0004', secret: 'D6A03F5C2E0E356F262D56F44370E1CD813583B2', credentials: '70BA33708CBFB103F1A8E34AFEF333BA7DC021022B2D9AAA583AABB8058D8D67', 'card[number]': '5500 0000 0000 0004', password: 'bFb7PBm6nZ7RJRq9', + oldpassword: 'oldSecret123', + newpassword: 'newSecret456', + 'old-password': 'oldSecretHyphen', + old_password: 'oldSecretUnderscore', + 'new-password': 'newSecretHyphen', + new_password: 'newSecretUnderscore', auth: 'C4CA4238A0B923820DCC509A6F75849B', - // eslint-disable-next-line @typescript-eslint/naming-convention access_token: '70BA33708CBFB103F1A8E34AFEF333BA7DC021022B2D9AAA583AABB8058D8D67', accessToken: '70BA33708CBFB103F1A8E34AFEF333BA7DC021022B2D9AAA583AABB8058D8D67', }; +/** + * Additional sensitive keys (newly added / previously uncovered). + * Keys intentionally use snake_case to match data-filter list. + */ +const additionalSensitiveDataMock = { + authorization: 'Bearer abc123', + token: 'token-value', + jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', + session: 'sess_xyz', + session_id: 'sid_789', + api_key: 'sk_live_xxx', + bearer: 'Bearer token', + client_secret: 'client_secret_value', + passwd: 'passwd_value', + mysql_pwd: 'mysql_pwd_value', + private_key: '-----BEGIN PRIVATE KEY-----', + ssh_key: 'ssh-rsa AAAA...', + card: '4111111111111111', + cardnumber: '5500000000000004', + creditcard: '4111111111111111', + pin: '1234', + security_code: '999', + stripetoken: 'tok_xxx', + cloudpayments_public_id: 'pk_xxx', + cloudpayments_secret: 'secret_xxx', + dsn: 'postgres://user:pass@host/db', + ssn: '123-45-6789', +}; +/* eslint-enable @typescript-eslint/naming-convention */ + describe('GrouperWorker', () => { const dataFilter = new DataFilter(); @@ -123,6 +160,34 @@ describe('GrouperWorker', () => { }); }); + test('should filter additional sensitive keys (authorization, token, payment, dsn, ssn, etc.) in context', async () => { + const event = generateEvent({ + context: additionalSensitiveDataMock, + }); + + dataFilter.processEvent(event); + + Object.keys(additionalSensitiveDataMock).forEach((key) => { + expect(event.context[key]).toBe('[filtered]'); + }); + }); + + test('should filter additional sensitive keys in addons', async () => { + const event = generateEvent({ + addons: { + vue: { + props: additionalSensitiveDataMock, + }, + }, + }); + + dataFilter.processEvent(event); + + Object.keys(additionalSensitiveDataMock).forEach((key) => { + expect(event.addons['vue']['props'][key]).toBe('[filtered]'); + }); + }); + test('should not replace values with keynames not in a list', async () => { const normalValue = 'test123'; const event = generateEvent({ @@ -154,7 +219,7 @@ describe('GrouperWorker', () => { const event = generateEvent({ context: { userId: uuidWithManyDigits, - sessionId: uuidUpperCase, + requestId: uuidUpperCase, transactionId: uuidNoDashes, }, addons: { @@ -169,7 +234,7 @@ describe('GrouperWorker', () => { dataFilter.processEvent(event); expect(event.context['userId']).toBe(uuidWithManyDigits); - expect(event.context['sessionId']).toBe(uuidUpperCase); + expect(event.context['requestId']).toBe(uuidUpperCase); expect(event.context['transactionId']).toBe(uuidNoDashes); expect(event.addons['vue']['props']['componentId']).toBe(uuidWithManyDigits); });