From d147abbcc67df4fd7ddffc3e650c3a332a75ba4a Mon Sep 17 00:00:00 2001 From: Carlos Trevino Date: Mon, 27 Jul 2026 13:18:48 -0700 Subject: [PATCH] fix: mask placeholder attributes when input masking is enabled When maskAllInputs or maskInputOptions is enabled, only input values were masked while placeholder attributes were left in plain text. This could leak PII if apps set placeholder text dynamically with user data. Now placeholder attributes on input and textarea elements are masked through the same maskInputValue() path, respecting maskInputOptions granularity and custom maskInputFn callbacks. Co-Authored-By: Claude Opus 4.6 --- packages/rrweb-snapshot/src/snapshot.ts | 13 ++++++ packages/rrweb/src/record/mutation.ts | 2 +- .../__snapshots__/integration.test.ts.snap | 30 ++++++++----- packages/rrweb/test/html/form.html | 4 +- packages/rrweb/test/integration.test.ts | 44 ++++++++++++++++++- 5 files changed, 79 insertions(+), 14 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 3d32a88a71..892f07571e 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -635,6 +635,19 @@ function serializeElementNode( attributes.checked = checked; } } + if ( + (tagName === 'input' || tagName === 'textarea') && + attributes.placeholder + ) { + attributes.placeholder = maskInputValue({ + element: n, + type: getInputType(n), + tagName, + value: attributes.placeholder as string, + maskInputOptions, + maskInputFn, + }); + } if (tagName === 'option') { if ((n as HTMLOptionElement).selected && !maskInputOptions['select']) { attributes.selected = true; diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 08e927a98f..c86607f94a 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -581,7 +581,7 @@ export default class MutationBuffer { let attributeName = m.attributeName as string; let value = (m.target as HTMLElement).getAttribute(attributeName); - if (attributeName === 'value') { + if (attributeName === 'value' || attributeName === 'placeholder') { const type = getInputType(target); value = maskInputValue({ diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 738f2fe8e2..4a80a16a18 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -2665,7 +2665,8 @@ exports[`record integration tests > can record form interactions 1`] = ` \\"type\\": 2, \\"tagName\\": \\"input\\", \\"attributes\\": { - \\"type\\": \\"text\\" + \\"type\\": \\"text\\", + \\"placeholder\\": \\"Enter your email\\" }, \\"childNodes\\": [], \\"id\\": 22 @@ -2806,7 +2807,8 @@ exports[`record integration tests > can record form interactions 1`] = ` \\"id\\": \\"\\", \\"cols\\": \\"30\\", \\"rows\\": \\"10\\", - \\"data-unmask-example\\": \\"true\\" + \\"data-unmask-example\\": \\"true\\", + \\"placeholder\\": \\"Tell us about yourself\\" }, \\"childNodes\\": [], \\"id\\": 42 @@ -4649,7 +4651,8 @@ exports[`record integration tests > can use maskInputOptions to configure which \\"type\\": 2, \\"tagName\\": \\"input\\", \\"attributes\\": { - \\"type\\": \\"text\\" + \\"type\\": \\"text\\", + \\"placeholder\\": \\"Enter your email\\" }, \\"childNodes\\": [], \\"id\\": 22 @@ -4790,7 +4793,8 @@ exports[`record integration tests > can use maskInputOptions to configure which \\"id\\": \\"\\", \\"cols\\": \\"30\\", \\"rows\\": \\"10\\", - \\"data-unmask-example\\": \\"true\\" + \\"data-unmask-example\\": \\"true\\", + \\"placeholder\\": \\"Tell us about yourself\\" }, \\"childNodes\\": [], \\"id\\": 42 @@ -6477,7 +6481,8 @@ exports[`record integration tests > should mask inputs via function call 1`] = ` \\"type\\": 2, \\"tagName\\": \\"input\\", \\"attributes\\": { - \\"type\\": \\"text\\" + \\"type\\": \\"text\\", + \\"placeholder\\": \\"****************\\" }, \\"childNodes\\": [], \\"id\\": 22 @@ -6618,7 +6623,8 @@ exports[`record integration tests > should mask inputs via function call 1`] = ` \\"id\\": \\"\\", \\"cols\\": \\"30\\", \\"rows\\": \\"10\\", - \\"data-unmask-example\\": \\"true\\" + \\"data-unmask-example\\": \\"true\\", + \\"placeholder\\": \\"Tell us about yourself\\" }, \\"childNodes\\": [], \\"id\\": 42 @@ -10305,7 +10311,8 @@ exports[`record integration tests > should not record input values if maskAllInp \\"type\\": 2, \\"tagName\\": \\"input\\", \\"attributes\\": { - \\"type\\": \\"text\\" + \\"type\\": \\"text\\", + \\"placeholder\\": \\"****************\\" }, \\"childNodes\\": [], \\"id\\": 22 @@ -10446,7 +10453,8 @@ exports[`record integration tests > should not record input values if maskAllInp \\"id\\": \\"\\", \\"cols\\": \\"30\\", \\"rows\\": \\"10\\", - \\"data-unmask-example\\": \\"true\\" + \\"data-unmask-example\\": \\"true\\", + \\"placeholder\\": \\"**********************\\" }, \\"childNodes\\": [], \\"id\\": 42 @@ -13418,7 +13426,8 @@ exports[`record integration tests > should record input userTriggered values if \\"type\\": 2, \\"tagName\\": \\"input\\", \\"attributes\\": { - \\"type\\": \\"text\\" + \\"type\\": \\"text\\", + \\"placeholder\\": \\"Enter your email\\" }, \\"childNodes\\": [], \\"id\\": 22 @@ -13559,7 +13568,8 @@ exports[`record integration tests > should record input userTriggered values if \\"id\\": \\"\\", \\"cols\\": \\"30\\", \\"rows\\": \\"10\\", - \\"data-unmask-example\\": \\"true\\" + \\"data-unmask-example\\": \\"true\\", + \\"placeholder\\": \\"Tell us about yourself\\" }, \\"childNodes\\": [], \\"id\\": 42 diff --git a/packages/rrweb/test/html/form.html b/packages/rrweb/test/html/form.html index 8480259fe0..7e643537a6 100644 --- a/packages/rrweb/test/html/form.html +++ b/packages/rrweb/test/html/form.html @@ -10,7 +10,7 @@