tracker: "secure by default" mode; 16.1.0

This commit is contained in:
nick-delirium 2025-03-14 16:32:26 +01:00 committed by Delirium
parent 4bac12308a
commit e7d309dadf
9 changed files with 31 additions and 13 deletions

View file

@ -1,3 +1,7 @@
## 16.1.0
- new `privateMode` option to hide all possible data from tracking
## 16.0.3 ## 16.0.3
- better handling for local svg spritemaps - better handling for local svg spritemaps

View file

@ -1,7 +1,7 @@
{ {
"name": "@openreplay/tracker", "name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package", "description": "The OpenReplay tracker main package",
"version": "16.0.3", "version": "16.1.0",
"keywords": [ "keywords": [
"logging", "logging",
"replay" "replay"

View file

@ -357,6 +357,9 @@ export default abstract class Observer {
if (name === 'href' || value.length > 1e5) { if (name === 'href' || value.length > 1e5) {
value = '' value = ''
} }
if (['alt', 'placeholder'].includes(name) && this.app.sanitizer.privateMode) {
value = value.replaceAll(/./g, '*')
}
this.app.attributeSender.sendSetAttribute(id, name, value) this.app.attributeSender.sendSetAttribute(id, name, value)
} }

View file

@ -41,12 +41,13 @@ export interface Options {
export const stringWiper = (input: string) => export const stringWiper = (input: string) =>
input input
.trim() .trim()
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '') .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '*')
export default class Sanitizer { export default class Sanitizer {
private readonly obscured: Set<number> = new Set() private readonly obscured: Set<number> = new Set()
private readonly hidden: Set<number> = new Set() private readonly hidden: Set<number> = new Set()
private readonly options: Options private readonly options: Options
public readonly privateMode: boolean
private readonly app: App private readonly app: App
constructor(params: { app: App; options?: Partial<Options> }) { constructor(params: { app: App; options?: Partial<Options> }) {
@ -57,16 +58,17 @@ export default class Sanitizer {
privateMode: false, privateMode: false,
domSanitizer: undefined, domSanitizer: undefined,
} }
this.privateMode = params.options?.privateMode ?? false
this.options = Object.assign(defaultOptions, params.options) this.options = Object.assign(defaultOptions, params.options)
} }
handleNode(id: number, parentID: number, node: Node) { handleNode(id: number, parentID: number, node: Node) {
if (this.options.privateMode) { if (this.options.privateMode) {
if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) { if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
this.obscured.add(id) return this.obscured.add(id)
} }
if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode as Element, 'unmask')) { if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode as Element, 'unmask')) {
this.obscured.add(id) return this.obscured.add(id)
} }
} }

View file

@ -108,9 +108,13 @@ export default function (app: App, opts: Partial<Options>): void {
return return
} }
const sendConsoleLog = app.safe((level: string, args: unknown[]): void => const sendConsoleLog = app.safe((level: string, args: unknown[]): void => {
app.send(ConsoleLog(level, printf(args))), let logMsg = printf(args)
) if (app.sanitizer.privateMode) {
logMsg = logMsg.replaceAll(/./g, '*')
}
app.send(ConsoleLog(level, logMsg))
})
let n = 0 let n = 0
const reset = (): void => { const reset = (): void => {

View file

@ -205,7 +205,10 @@ export default function (app: App, opts: Partial<Options>): void {
inputTime: number, inputTime: number,
) { ) {
const { value, mask } = getInputValue(id, node) const { value, mask } = getInputValue(id, node)
const label = getInputLabel(node) let label = getInputLabel(node)
if (app.sanitizer.privateMode) {
label = label.replaceAll(/./g, '*')
}
app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime)) app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime))
} }

View file

@ -230,11 +230,12 @@ export default function (app: App, options?: MouseHandlerOptions): void {
const normalizedY = roundNumber(clickY / contentHeight) const normalizedY = roundNumber(clickY / contentHeight)
sendMouseMove() sendMouseMove()
const label = getTargetLabel(target)
app.send( app.send(
MouseClick( MouseClick(
id, id,
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
getTargetLabel(target), app.sanitizer.privateMode ? label.replaceAll(/./g, '*') : label,
isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : '', isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : '',
normalizedX, normalizedX,
normalizedY, normalizedY,

View file

@ -101,7 +101,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
} }
function sanitize(reqResInfo: RequestResponseData) { function sanitize(reqResInfo: RequestResponseData) {
if (!options.capturePayload) { if (!options.capturePayload || app.sanitizer.privateMode) {
// @ts-ignore // @ts-ignore
delete reqResInfo.request.body delete reqResInfo.request.body
delete reqResInfo.response.body delete reqResInfo.response.body
@ -136,18 +136,19 @@ export default function (app: App, opts: Partial<Options> = {}) {
if (options.useProxy) { if (options.useProxy) {
return createNetworkProxy( return createNetworkProxy(
context, context,
options.ignoreHeaders, app.sanitizer.privateMode ? true : options.ignoreHeaders,
setSessionTokenHeader, setSessionTokenHeader,
sanitize, sanitize,
(message) => { (message) => {
if (options.failuresOnly && message.status < 400) { if (options.failuresOnly && message.status < 400) {
return return
} }
const url = app.sanitizer.privateMode ? '************' : message.url
app.send( app.send(
NetworkRequest( NetworkRequest(
message.requestType, message.requestType,
message.method, message.method,
message.url, url,
message.request, message.request,
message.response, message.response,
message.status, message.status,

View file

@ -147,7 +147,7 @@ export default function (app: App, opts: Partial<Options>): void {
entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0, entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
entry.encodedBodySize || 0, entry.encodedBodySize || 0,
entry.decodedBodySize || 0, entry.decodedBodySize || 0,
entry.name, app.sanitizer.privateMode ? entry.name.replaceAll(/./g, '*') : entry.name,
entry.initiatorType, entry.initiatorType,
entry.transferSize, entry.transferSize,
// @ts-ignore // @ts-ignore