From d3fe7da5fdc7d61a3b6e11908a3ad70bde03f0f8 Mon Sep 17 00:00:00 2001 From: John Pham Date: Fri, 4 Feb 2022 10:16:18 -0800 Subject: [PATCH] Make sure text mutations respect strict privacy mode --- package.json | 2 +- src/record/mutation.ts | 15 +++++++++++---- src/snapshot/snapshot.ts | 12 ++---------- src/utils.ts | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 81ab58b1..26aa74cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@highlight-run/rrweb", - "version": "1.1.7", + "version": "1.1.8", "description": "record and replay the web", "scripts": { "test": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register -r ignore-styles -r jsdom-global/register test/**.test.ts", diff --git a/src/record/mutation.ts b/src/record/mutation.ts index 11daa85d..549ea01d 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -29,6 +29,7 @@ import { isIgnored, isIframeINode, hasShadowRoot, + obfuscateText, } from '../utils'; import { IframeManager } from './iframe-manager'; import { CanvasManager } from './observers/canvas/canvas-manager'; @@ -406,10 +407,16 @@ export default class MutationBuffer { const payload = { texts: this.texts - .map((text) => ({ - id: this.mirror.getId(text.node as INode), - value: text.value, - })) + .map((text) => { + let value = text.value; + if (this.enableStrictPrivacy && value) { + value = obfuscateText(value); + } + return { + id: this.mirror.getId(text.node as INode), + value, + }; + }) // text mutation's id was not in the mirror map means the target node has been removed .filter((text) => this.mirror.has(text.id)), attributes: this.attributes diff --git a/src/snapshot/snapshot.ts b/src/snapshot/snapshot.ts index 33a096b4..6e4c544a 100644 --- a/src/snapshot/snapshot.ts +++ b/src/snapshot/snapshot.ts @@ -1,3 +1,4 @@ +import { obfuscateText } from '../utils'; import { serializedNode, serializedNodeWithId, @@ -625,16 +626,7 @@ function serializeNode( 'NOSCRIPT', ]); if (!IGNORE_TAG_NAMES.has(parentTagName) && textContent) { - // We remove non-printing characters. - // For example: '‌' is a character that isn't shown visibly or takes up layout space on the screen. However if you take the length of the string, it's counted as 1. - // For example: "‌1"'s length is 2 but visually it's only taking up 1 character width. - // If we don't filter does out, our string obfuscation could have more characters than what was originally presented. - textContent = textContent.replace(/[^ -~]+/g, ''); - textContent = - textContent - ?.split(' ') - .map((word) => Math.random().toString(20).substr(2, word.length)) - .join(' ') || ''; + textContent = obfuscateText(textContent); } } return { diff --git a/src/utils.ts b/src/utils.ts index c1f91452..b94fe97c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -629,3 +629,20 @@ export function hasShadowRoot( ): n is T & { shadowRoot: ShadowRoot } { return Boolean(((n as unknown) as Element)?.shadowRoot); } + +/** + * Returns a string of the same length that has been obfuscated. + */ +export function obfuscateText(text: string): string { + // We remove non-printing characters. + // For example: '‌' is a character that isn't shown visibly or takes up layout space on the screen. However if you take the length of the string, it's counted as 1. + // For example: "‌1"'s length is 2 but visually it's only taking up 1 character width. + // If we don't filter does out, our string obfuscation could have more characters than what was originally presented. + text = text.replace(/[^ -~]+/g, ''); + text = + text + ?.split(' ') + .map((word) => Math.random().toString(20).substr(2, word.length)) + .join(' ') || ''; + return text; +}