diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 04a7383c16..f89cd919e5 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -383,6 +383,24 @@ export function needMaskingText( : node.parentElement; if (el === null) return false; + if (el.tagName === 'INPUT') { + // Special cases: We want to enforce some masking for password & credit-card related fields, + // no matter the settings + const autocomplete = el.getAttribute('autocomplete'); + const disallowedAutocompleteValues = [ + 'current-password', + 'new-password', + 'cc-number', + 'cc-exp', + 'cc-exp-month', + 'cc-exp-year', + 'cc-csc', + ]; + if (disallowedAutocompleteValues.includes(autocomplete as string)) { + return true; + } + } + let maskDistance = -1; let unmaskDistance = -1; diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 30fa232b9d..5412826dac 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -277,25 +277,25 @@ exports[`integration tests [html file]: form-fields-sensitive.html 1`] = ` " @@ -313,25 +313,25 @@ exports[`integration tests [html file]: form-fields-sensitive-update.html 1`] = diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index f103baa6e2..7a54b4e6b4 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -497,4 +497,34 @@ describe('needMaskingText', () => { ), ).toEqual(false); }); + + describe('enforced masking', () => { + it.each([ + 'current-password', + 'new-password', + 'cc-number', + 'cc-exp', + 'cc-exp-month', + 'cc-exp-year', + 'cc-csc', + ])('enforces masking for autocomplete="%s"', (autocompleteValue) => { + document.write( + ``, + ); + const el = document.querySelector('input')!; + expect( + needMaskingText(el, 'maskmask', '.foo', 'unmaskmask', null, false), + ).toEqual(true); + }); + + it('does not mask other autocomplete values', () => { + document.write( + ``, + ); + const el = document.querySelector('input')!; + expect( + needMaskingText(el, 'maskmask', '.foo', 'unmaskmask', null, false), + ).toEqual(false); + }); + }); }); diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 79919c964f..5386b192d0 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -12643,6 +12643,656 @@ exports[`record integration tests should not record input events on ignored elem ]" `; +exports[`record integration tests should not record input values for inputs we enforce 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"name\\", + \\"value\\": \\"allowed value\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + } + }, + { + \\"parentId\\": 4, + \\"nextId\\": 9, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"cc-csc\\", + \\"value\\": \\"*************\\" + }, + \\"childNodes\\": [], + \\"id\\": 10 + } + }, + { + \\"parentId\\": 4, + \\"nextId\\": 10, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"cc-exp-year\\", + \\"value\\": \\"*************\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + } + }, + { + \\"parentId\\": 4, + \\"nextId\\": 11, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"cc-exp-month\\", + \\"value\\": \\"*************\\" + }, + \\"childNodes\\": [], + \\"id\\": 12 + } + }, + { + \\"parentId\\": 4, + \\"nextId\\": 12, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"cc-exp\\", + \\"value\\": \\"*************\\" + }, + \\"childNodes\\": [], + \\"id\\": 13 + } + }, + { + \\"parentId\\": 4, + \\"nextId\\": 13, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"cc-number\\", + \\"value\\": \\"*************\\" + }, + \\"childNodes\\": [], + \\"id\\": 14 + } + }, + { + \\"parentId\\": 4, + \\"nextId\\": 14, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"new-password\\", + \\"value\\": \\"*************\\" + }, + \\"childNodes\\": [], + \\"id\\": 15 + } + }, + { + \\"parentId\\": 4, + \\"nextId\\": 15, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"autocomplete\\": \\"current-password\\", + \\"value\\": \\"*************\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 16 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 15 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 14 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 13 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 12 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 11 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************\\", + \\"isChecked\\": false, + \\"id\\": 10 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed value\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 16 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**************\\", + \\"isChecked\\": false, + \\"id\\": 16 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***************\\", + \\"isChecked\\": false, + \\"id\\": 16 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****************\\", + \\"isChecked\\": false, + \\"id\\": 16 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 16 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 15 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**************\\", + \\"isChecked\\": false, + \\"id\\": 15 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***************\\", + \\"isChecked\\": false, + \\"id\\": 15 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****************\\", + \\"isChecked\\": false, + \\"id\\": 15 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 15 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 14 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**************\\", + \\"isChecked\\": false, + \\"id\\": 14 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***************\\", + \\"isChecked\\": false, + \\"id\\": 14 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****************\\", + \\"isChecked\\": false, + \\"id\\": 14 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 14 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 13 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**************\\", + \\"isChecked\\": false, + \\"id\\": 13 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***************\\", + \\"isChecked\\": false, + \\"id\\": 13 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****************\\", + \\"isChecked\\": false, + \\"id\\": 13 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 13 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 12 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**************\\", + \\"isChecked\\": false, + \\"id\\": 12 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***************\\", + \\"isChecked\\": false, + \\"id\\": 12 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****************\\", + \\"isChecked\\": false, + \\"id\\": 12 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 12 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 11 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**************\\", + \\"isChecked\\": false, + \\"id\\": 11 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***************\\", + \\"isChecked\\": false, + \\"id\\": 11 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****************\\", + \\"isChecked\\": false, + \\"id\\": 11 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 11 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 10 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**************\\", + \\"isChecked\\": false, + \\"id\\": 10 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***************\\", + \\"isChecked\\": false, + \\"id\\": 10 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"****************\\", + \\"isChecked\\": false, + \\"id\\": 10 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 10 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed valuea\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed valueal\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed valueall\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed valueallo\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed valueallow\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed valueallowe\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"allowed valueallowed\\", + \\"isChecked\\": false, + \\"id\\": 9 + } + } +]" +`; + exports[`record integration tests should not record input values if dynamically added and maskAllInputs is true 1`] = ` "[ { diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index c20adc2494..b196f39b0e 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -401,6 +401,52 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should not record input values for inputs we enforce', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'blank.html')); + + // Dynamically add elements to page + await page.evaluate(() => { + const autocompleteValues = [ + 'current-password', + 'new-password', + 'cc-number', + 'cc-exp', + 'cc-exp-month', + 'cc-exp-year', + 'cc-csc', + ]; + const parent = document.body; + autocompleteValues.forEach((autocomplete) => { + const el = document.createElement('input'); + el.setAttribute('autocomplete', autocomplete); + el.value = 'initial value'; + parent.appendChild(el); + }); + + // this one is allowed + const el = document.createElement('input'); + el.setAttribute('autocomplete', 'name'); + el.value = 'allowed value'; + parent.appendChild(el); + }); + + await page.type('input[autocomplete="current-password"]', 'new'); + await page.type('input[autocomplete="new-password"]', 'new'); + await page.type('input[autocomplete="cc-number"]', 'new'); + await page.type('input[autocomplete="cc-exp"]', 'new'); + await page.type('input[autocomplete="cc-exp-month"]', 'new'); + await page.type('input[autocomplete="cc-exp-year"]', 'new'); + await page.type('input[autocomplete="cc-csc"]', 'new'); + await page.type('input[autocomplete="name"]', 'allowed'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + it('can use maskInputOptions to configure which type of inputs should be masked', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank');