Skip to content

feat: Privacy at Capture — recorded-DOM URL sanitization (experimental) - #9

Open
roggernaut wants to merge 4 commits into
privacy-at-capture-corefrom
privacy-at-capture-url-sanitization
Open

feat: Privacy at Capture — recorded-DOM URL sanitization (experimental)#9
roggernaut wants to merge 4 commits into
privacy-at-capture-corefrom
privacy-at-capture-url-sanitization

Conversation

@roggernaut

@roggernaut roggernaut commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Under balanced and strict, every URL-bearing attribute the serializer emits (action, background, data, formaction, href, poster, rr_src, src, xlink:href) and the Meta event's href go through sanitizeUrl:

  • userinfo (username/password) is stripped
  • the value of any query parameter in a default sensitive list (access_token, auth, code, key, password, secret, session, token) plus anything in url.blockedQueryParameters is replaced with *
  • the hash is removed unless url.removeHash: false
  • strict blocks every parameter's value unless url.allowedQueryParameters names it explicitly

Parameter names stay visible so replays keep their routing context. An unparseable URL fails closed: the attribute is dropped (null) rather than emptied, since an empty src/href re-resolves to the document URL at replay and actually gets requested. The unmask escape cannot reopen a sanitized URL.

rr_src — the name the serializer gives a cross-origin <iframe src> it cannot see into — is in the URL attribute set, because the rename happens before finalization and the renamed attribute would otherwise escape the policy entirely.

No vendor precedent — review this hardest

Unlike everything else in Privacy at Capture (#4), no session-replay vendor sanitizes URLs in the recorded DOM. PostHog, Highlight, Sentry, Amplitude and Mixpanel all record href and src verbatim and scrub URLs in their ingestion pipeline instead, if at all. This is rrweb's own design rather than an established pattern, and it rewrites attribute values that the replay then depends on.

This PR layers directly on #4 and is independent of the detectors PR (#8) — neither depends on the other's code.

Tests

  • packages/rrweb-snapshot — 231 passed, 1 skipped
  • packages/rrweb test/record/privacy.test.ts + test/record.test.ts — passing headless (current counts in CI)
  • npx tsc -b clean

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Bundle Size Changes

Size change: +39.54 kB (+0.36%) | Total size: 11069.58 kB

all - 2101.05 kB -> 2109.18 kB (+8.12 kB (+0.39%))
File Base PR Diff
all.cjs 603.81 kB 606.13 kB +2.32 kB (+0.38%)
all.js 603.48 kB 605.80 kB +2.32 kB (+0.38%)
all.umd.cjs 607.35 kB 609.71 kB +2.36 kB (+0.39%)
all.umd.min.cjs 286.41 kB 287.54 kB +1.13 kB (+0.39%)
browser-client - 794.48 kB -> 802.59 kB (+8.11 kB (+1.02%))
File Base PR Diff
browser-client.cjs 227.78 kB 230.09 kB +2.31 kB (+1.01%)
browser-client.js 227.55 kB 229.86 kB +2.31 kB (+1.02%)
browser-client.umd.cjs 230.94 kB 233.29 kB +2.35 kB (+1.02%)
browser-client.umd.min.cjs 108.22 kB 109.35 kB +1.13 kB (+1.05%)
record - 676.87 kB -> 684.98 kB (+8.10 kB (+1.20%))
File Base PR Diff
record.cjs 194.29 kB 196.60 kB +2.31 kB (+1.19%)
record.js 194.20 kB 196.51 kB +2.31 kB (+1.19%)
record.umd.cjs 195.31 kB 197.66 kB +2.35 kB (+1.20%)
record.umd.min.cjs 93.07 kB 94.21 kB +1.13 kB (+1.22%)
rrweb - 2039.66 kB -> 2047.77 kB (+8.11 kB (+0.40%))
File Base PR Diff
rrweb.cjs 587.18 kB 589.49 kB +2.32 kB (+0.39%)
rrweb.js 586.88 kB 589.19 kB +2.32 kB (+0.39%)
rrweb.umd.cjs 588.52 kB 590.87 kB +2.35 kB (+0.40%)
rrweb.umd.min.cjs 277.09 kB 278.22 kB +1.13 kB (+0.41%)
rrweb-snapshot - 702.08 kB -> 709.17 kB (+7.09 kB (+1.01%))
File Base PR Diff
rrweb-snapshot.cjs 201.81 kB 203.80 kB +1.99 kB (+0.99%)
rrweb-snapshot.js 200.40 kB 202.35 kB +1.95 kB (+0.97%)
rrweb-snapshot.umd.cjs 204.43 kB 206.42 kB +1.99 kB (+0.98%)
rrweb-snapshot.umd.min.cjs 95.45 kB 96.61 kB +1.16 kB (+1.21%)

@roggernaut
roggernaut force-pushed the privacy-at-capture-url-sanitization branch from ca4f14c to 7c1769d Compare September 2, 2026 14:33
@roggernaut
roggernaut force-pushed the privacy-at-capture-core branch from ee793c7 to da75a0f Compare September 2, 2026 19:51
@roggernaut
roggernaut force-pushed the privacy-at-capture-url-sanitization branch from 7c1769d to 11c2f29 Compare September 2, 2026 21:06
Rogier Trimpe and others added 4 commits September 3, 2026 12:11
…perimental)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…strict

Add sanitizeMetaUrl (blocked-list-only param masking) for the Meta event's
window.location.href call site in record/index.ts, so strict's
mask-every-param-unless-allowlisted rule applies only to DOM URL
attributes -- the Meta href is the recording's own address, not markup a
page author wrote, so masking every one of its params (most apps' own
routing state) would make strict useless for identifying which page a
session happened on. Implemented as an internal paramsMode option on
sanitizeUrl so the special case stays inside this layer; core has no
sanitizeUrl branch. Flagged in guide.md and the changeset as an open
design question for upstream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…root-relative

Sanitization parses against an internal base URL and re-serializes the path,
so a relative URL in an attribute rrweb does not already absolutify
(`<form action>`, `<video poster>`, ...) is recorded root-relative:
`pay/confirm` becomes `/pay/confirm`. Behavior unchanged; this was
undocumented.

refactor(privacy): rename the `manual` preset to `minimal` throughout the
url-sanitization layer's own additions -- the `sanitizeUrl` test fixtures
and test names. Clean rename, nothing released; the record()-level
"manual selector"/"manual half" test names are unrelated and untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@roggernaut
roggernaut force-pushed the privacy-at-capture-url-sanitization branch from 11c2f29 to cd0539b Compare September 3, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant