Skip to content

Commit b4261e7

Browse files
authored
fix: Fix some input masking & align with upstream (#85)
This brings a few further fixes to input masking (esp. for radio buttons), which we also fixed/are in the process of fixing upstream. Also this aligns some things with how they have been fixed upstream, to make a future merging easier.
1 parent 78dcb80 commit b4261e7

File tree

6 files changed

+89
-55
lines changed

6 files changed

+89
-55
lines changed

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import {
1414
ICanvas,
1515
} from './types';
1616
import {
17+
getInputValue,
1718
is2DCanvasBlank,
1819
isElement,
1920
isShadowRoot,
2021
maskInputValue,
22+
getInputType,
2123
} from './utils';
2224

2325
let _id = 1;
@@ -593,17 +595,18 @@ function serializeNode(
593595
| HTMLTextAreaElement
594596
| HTMLSelectElement
595597
| HTMLOptionElement;
596-
const value = getInputValue(tagName, el, attributes);
598+
const type = getInputType(el);
599+
const value = getInputValue(el, tagName.toUpperCase() as unknown as Uppercase<typeof tagName>, type);
597600
const checked = (n as HTMLInputElement).checked;
598601

599602
if (
600-
attributes.type !== 'submit' &&
601-
attributes.type !== 'button' &&
603+
type !== 'submit' &&
604+
type !== 'button' &&
602605
value
603606
) {
604607
attributes.value = maskInputValue({
605608
input: el,
606-
type: attributes.type,
609+
type,
607610
tagName,
608611
value,
609612
maskInputSelector,
@@ -1309,23 +1312,3 @@ function skipAttribute(
13091312
);
13101313
}
13111314

1312-
function getInputValue(
1313-
tagName: string,
1314-
el:
1315-
| HTMLInputElement
1316-
| HTMLTextAreaElement
1317-
| HTMLSelectElement
1318-
| HTMLOptionElement,
1319-
attributes: attributes,
1320-
): string {
1321-
if (
1322-
tagName === 'input' &&
1323-
(attributes.type === 'radio' || attributes.type === 'checkbox')
1324-
) {
1325-
// checkboxes & radio buttons return `on` as their el.value when no value is specified
1326-
// we only want to get the value if it is specified as `value='xxx'`
1327-
return el.getAttribute('value') || '';
1328-
}
1329-
1330-
return el.value;
1331-
}

packages/rrweb-snapshot/src/utils.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { INode, MaskInputFn, MaskInputOptions } from './types';
1+
import { attributes, INode, MaskInputFn, MaskInputOptions } from './types';
22

33
export function isElement(n: Node | INode): n is Element {
44
return n.nodeType === n.ELEMENT_NODE;
@@ -82,7 +82,7 @@ export function maskInputValue({
8282
return text;
8383
}
8484

85-
if (input.hasAttribute('rr_is_password')) {
85+
if (input.hasAttribute('data-rr-is-password')) {
8686
type = 'password';
8787
}
8888

@@ -136,3 +136,38 @@ export function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean {
136136
}
137137
return true;
138138
}
139+
140+
/**
141+
* Get the type of an input element.
142+
* This takes care of the case where a password input is changed to a text input.
143+
* In this case, we continue to consider this of type password, in order to avoid leaking sensitive data
144+
* where passwords should be masked.
145+
*/
146+
export function getInputType(element: HTMLElement): Lowercase<string> | null {
147+
const type = (element as HTMLInputElement).type;
148+
149+
return element.hasAttribute('data-rr-is-password')
150+
? 'password'
151+
: type
152+
? (type.toLowerCase() as Lowercase<string>)
153+
: null;
154+
}
155+
156+
export function getInputValue(
157+
el:
158+
| HTMLInputElement
159+
| HTMLTextAreaElement
160+
| HTMLSelectElement
161+
| HTMLOptionElement,
162+
tagName: Uppercase<string>,
163+
type: attributes[string],
164+
): string {
165+
const normalizedType = typeof type === 'string' ? type.toLowerCase() : '';
166+
if (tagName === 'INPUT' && (type === 'radio' || type === 'checkbox')) {
167+
// checkboxes & radio buttons return `on` as their el.value when no value is specified
168+
// we only want to get the value if it is specified as `value='xxx'`
169+
return el.getAttribute('value') || '';
170+
}
171+
172+
return el.value;
173+
}

packages/rrweb-snapshot/typings/utils.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { INode, MaskInputFn, MaskInputOptions } from './types';
1+
import { attributes, INode, MaskInputFn, MaskInputOptions } from './types';
22
export declare function isElement(n: Node | INode): n is Element;
33
export declare function isShadowRoot(n: Node): n is ShadowRoot;
44
interface IsInputTypeMasked {
@@ -18,4 +18,6 @@ interface MaskInputValue extends HasInputMaskOptions {
1818
}
1919
export declare function maskInputValue({ input, maskInputSelector, unmaskInputSelector, maskInputOptions, tagName, type, value, maskInputFn, }: MaskInputValue): string;
2020
export declare function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean;
21+
export declare function getInputType(element: HTMLElement): Lowercase<string> | null;
22+
export declare function getInputValue(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLOptionElement, tagName: Uppercase<string>, type: attributes[string]): string;
2123
export {};

packages/rrweb/src/record/mutation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,10 +507,10 @@ export default class MutationBuffer {
507507
// This is used to ensure we do not unmask value when using e.g. a "Show password" type button
508508
if (
509509
m.attributeName === 'type' &&
510-
(m.target as HTMLElement).tagName === 'INPUT' &&
510+
target.tagName === 'INPUT' &&
511511
(m.oldValue || '').toLowerCase() === 'password'
512512
) {
513-
(m.target as HTMLElement).setAttribute('rr_is_password', 'true');
513+
target.setAttribute('data-rr-is-password', 'true');
514514
}
515515

516516
if (m.attributeName === 'style') {

packages/rrweb/src/record/observer.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
INode,
33
hasInputMaskOptions,
44
maskInputValue,
5+
getInputType,
6+
getInputValue,
57
} from '@sentry-internal/rrweb-snapshot';
68
import { FontFaceSet } from 'css-font-loading-module';
79
import {
@@ -361,7 +363,8 @@ function initInputObserver({
361363
}: observerParam): listenerHandler {
362364
function eventHandler(event: Event) {
363365
let target = getEventTarget(event);
364-
const tagName = target && (target as Element).tagName;
366+
const tagName =
367+
target && (((target as Element).tagName as unknown) as Uppercase<string>);
365368

366369
const userTriggered = event.isTrusted;
367370
/**
@@ -377,24 +380,25 @@ function initInputObserver({
377380
) {
378381
return;
379382
}
380-
let type: string | undefined = (target as HTMLInputElement).type;
383+
const el = target as
384+
| HTMLInputElement
385+
| HTMLSelectElement
386+
| HTMLTextAreaElement;
387+
const type = getInputType(el);
381388
if (
382-
(target as HTMLElement).classList.contains(ignoreClass) ||
383-
(ignoreSelector && (target as HTMLElement).matches(ignoreSelector))
389+
el.classList.contains(ignoreClass) ||
390+
(ignoreSelector && el.matches(ignoreSelector))
384391
) {
385392
return;
386393
}
387394

388-
let text = (target as HTMLInputElement).value;
395+
let text = getInputValue(el, tagName, type);
389396
let isChecked = false;
390397

391-
if ((target as HTMLElement).hasAttribute('rr_is_password')) {
392-
type = 'password';
393-
}
394-
395398
if (type === 'radio' || type === 'checkbox') {
396399
isChecked = (target as HTMLInputElement).checked;
397-
} else if (
400+
}
401+
if (
398402
hasInputMaskOptions({
399403
maskInputOptions,
400404
maskInputSelector,
@@ -403,7 +407,7 @@ function initInputObserver({
403407
})
404408
) {
405409
text = maskInputValue({
406-
input: target as HTMLElement,
410+
input: el,
407411
maskInputOptions,
408412
maskInputSelector,
409413
unmaskInputSelector,
@@ -428,11 +432,21 @@ function initInputObserver({
428432
.querySelectorAll(`input[type="radio"][name="${name}"]`)
429433
.forEach((el) => {
430434
if (el !== target) {
435+
const text = maskInputValue({
436+
input: el as HTMLInputElement,
437+
maskInputOptions,
438+
maskInputSelector,
439+
unmaskInputSelector,
440+
tagName,
441+
type,
442+
value: getInputValue(el as HTMLInputElement, tagName, type),
443+
maskInputFn,
444+
});
431445
cbWithDedup(
432446
el,
433447
callbackWrapper(wrapEventWithUserTriggeredFlag)(
434448
{
435-
text: (el as HTMLInputElement).value,
449+
text,
436450
isChecked: !isChecked,
437451
userTriggered: false,
438452
},

packages/rrweb/test/__snapshots__/integration.test.ts.snap

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,7 +1805,7 @@ exports[`record integration tests can record form interactions 1`] = `
18051805
"type": 3,
18061806
"data": {
18071807
"source": 5,
1808-
"text": "on",
1808+
"text": "",
18091809
"isChecked": true,
18101810
"id": 32
18111811
}
@@ -1872,7 +1872,7 @@ exports[`record integration tests can record form interactions 1`] = `
18721872
"type": 3,
18731873
"data": {
18741874
"source": 5,
1875-
"text": "on",
1875+
"text": "",
18761876
"isChecked": false,
18771877
"id": 47
18781878
}
@@ -3792,7 +3792,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty
37923792
"type": 3,
37933793
"data": {
37943794
"source": 5,
3795-
"text": "on",
3795+
"text": "",
37963796
"isChecked": true,
37973797
"id": 32
37983798
}
@@ -3859,7 +3859,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty
38593859
"type": 3,
38603860
"data": {
38613861
"source": 5,
3862-
"text": "on",
3862+
"text": "",
38633863
"isChecked": false,
38643864
"id": 47
38653865
}
@@ -4999,7 +4999,7 @@ exports[`record integration tests should always mask value attribute of password
49994999
{
50005000
"id": 18,
50015001
"attributes": {
5002-
"rr_is_password": "true"
5002+
"data-rr-is-password": "true"
50035003
}
50045004
}
50055005
],
@@ -10223,7 +10223,7 @@ exports[`record integration tests should not record input values if maskAllInput
1022310223
"type": 3,
1022410224
"data": {
1022510225
"source": 5,
10226-
"text": "on",
10226+
"text": "",
1022710227
"isChecked": true,
1022810228
"id": 32
1022910229
}
@@ -10232,7 +10232,7 @@ exports[`record integration tests should not record input values if maskAllInput
1023210232
"type": 3,
1023310233
"data": {
1023410234
"source": 5,
10235-
"text": "radio-on",
10235+
"text": "********",
1023610236
"isChecked": false,
1023710237
"id": 37
1023810238
}
@@ -10241,7 +10241,7 @@ exports[`record integration tests should not record input values if maskAllInput
1024110241
"type": 3,
1024210242
"data": {
1024310243
"source": 5,
10244-
"text": "radio-off",
10244+
"text": "*********",
1024510245
"isChecked": false,
1024610246
"id": 42
1024710247
}
@@ -10290,7 +10290,7 @@ exports[`record integration tests should not record input values if maskAllInput
1029010290
"type": 3,
1029110291
"data": {
1029210292
"source": 5,
10293-
"text": "on",
10293+
"text": "",
1029410294
"isChecked": false,
1029510295
"id": 47
1029610296
}
@@ -11180,7 +11180,7 @@ exports[`record integration tests should not record input values on selectively
1118011180
"type": 3,
1118111181
"data": {
1118211182
"source": 5,
11183-
"text": "on",
11183+
"text": "**",
1118411184
"isChecked": true,
1118511185
"id": 27
1118611186
}
@@ -11189,7 +11189,7 @@ exports[`record integration tests should not record input values on selectively
1118911189
"type": 3,
1119011190
"data": {
1119111191
"source": 5,
11192-
"text": "off",
11192+
"text": "***",
1119311193
"isChecked": false,
1119411194
"id": 32
1119511195
}
@@ -11238,7 +11238,7 @@ exports[`record integration tests should not record input values on selectively
1123811238
"type": 3,
1123911239
"data": {
1124011240
"source": 5,
11241-
"text": "on",
11241+
"text": "",
1124211242
"isChecked": true,
1124311243
"id": 37
1124411244
}
@@ -14469,7 +14469,7 @@ exports[`record integration tests should record input userTriggered values if us
1446914469
"type": 3,
1447014470
"data": {
1447114471
"source": 5,
14472-
"text": "on",
14472+
"text": "",
1447314473
"isChecked": true,
1447414474
"userTriggered": true,
1447514475
"id": 32
@@ -14539,7 +14539,7 @@ exports[`record integration tests should record input userTriggered values if us
1453914539
"type": 3,
1454014540
"data": {
1454114541
"source": 5,
14542-
"text": "on",
14542+
"text": "",
1454314543
"isChecked": false,
1454414544
"userTriggered": true,
1454514545
"id": 47

0 commit comments

Comments
 (0)