Skip to content

fix(replay): rebuild canvas args only from known constructors, behind a config option - #39

Merged
mayberryzane merged 2 commits into
mainfrom
canvas-arg-constructor-allowlist
Aug 18, 2026
Merged

mayberryzane merged 2 commits into
mainfrom
canvas-arg-constructor-allowlist

Conversation

@mayberryzane

@mayberryzane mayberryzane commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

What

deserializeArg rebuilt a serialized canvas arg by looking its rr_type up on window and calling it as a constructor. This adds an explicit set of the constructors the recorder actually emits (see record/observers/canvas/serialize-args.ts for the matching serialization):

  • the nine typed arrays, DataView, ImageData
  • ArrayBuffer — normally serialized as base64, but reachable in args form from older clients and nested inside DataView args
  • Array — the nested-arg wrapper older clients emit (covered by the existing preloadAllImages tests)

Anything else deserializes to null and logs [replayer] refusing to construct canvas arg of unknown type: <name>, so a missing entry shows up in the replayer console instead of a silently blank canvas.

null rather than a throw is deliberate: deserializeArg runs inside preloadAllImages and inside an un-caught Promise.all in replay/canvas/2d.ts, so one odd arg shouldn't take down preload for an entire session. The existing warnCanvasMutationFailed handler already covers the downstream canvas call failing.

Opt-in

The second commit puts this behind a new enforceCanvasArgAllowlist player config option, defaulting to false — current behavior is unchanged unless an embedding app opts in, so it can be rolled out and rolled back without shipping a new bundle.

It's threaded from playerConfig through the canvas mutation dispatcher into deserializeArg, covering both the mutation path (replay/canvas/{index,2d,webgl}.ts) and the eager preload path (deserializeAndPreloadCanvasEvents) — the latter matters because preload caches its deserialized args into canvasEventMap, which the mutation path then reuses.

It's also in SERIALIZABLE_CONFIG_KEYS, so the option survives the postMessage boundary into the embedded replayer host. Without that entry pickSerializableConfig would silently drop it and origin-isolated replay would never see it.

Replay-side only — nothing in record/ changes.

Not affected

ImageBitmap, HTMLImageElement, Blob and the WebGL variable references are handled by the rr_type === 'ImageBitmap', src, data and index branches, none of which reach the constructor path. There are tests asserting those still work with enforcement on.

Tests

packages/rrweb/test/replay/deserialize-args.test.ts — 33 tests, up from 13:

  • with enforcement on: each of the nine typed arrays, ImageData, DataView over a base64 ArrayBuffer, and the Array wrapper all round-trip; the src and data branches are untouched
  • with enforcement on: unknown types deserialize to null, including nested in an arg list, and a string argument to an unknown type is never evaluated
  • with enforcement off (the default): nothing is filtered

test/replay/embedded.test.ts — pickSerializableConfig keeps the new key.

The negative tests were confirmed failing against the unpatched deserializer before the fix went in, and the whole thing was exercised against the built UMD bundle in Chrome 115 with both flag states.

Verified locally on macOS:

  • vitest run test/replay test/replayer.test.ts — 140 passed, 1 skipped, incl. the webgl replay, embedded-replayer e2e, and painted-canvas-in-iframe tests
  • tsc -noEmit clean; eslint packages/rrweb/src adds no new warnings; prettier --check clean

Worth noting: the existing preloadAllImages suite is what surfaced the Array wrapper, which an allowlist derived from serialize-args.ts alone would have missed.

🤖 Generated with Claude Code


Note

Overview
Canvas replay deserialization no longer treats every serialized rr_type as a window constructor. deserializeArg now supports an optional allowlist of recorder-emitted types (typed arrays, DataView, ImageData, ArrayBuffer, Array); when enforcement is on, unknown types return null and log a replayer warning instead of instantiating arbitrary globals.

The behavior is gated by a new enforceCanvasArgAllowlist player config (default false). The flag is passed through 2D/WebGL canvas mutation paths, eager deserializeAndPreloadCanvasEvents preload (so canvasEventMap stays consistent), and SERIALIZABLE_CONFIG_KEYS for embedded replay over postMessage.

Tests cover allowlisted round-trips, rejection of types like Function/Object (including nested args and non-evaluated string payloads), and unchanged behavior when enforcement is off.

Reviewed by Cursor Bugbot for commit 56fce23. Bugbot is set up for automated code reviews on this repo. Configure here.

mayberryzane and others added 2 commits August 18, 2026 12:03
Match a serialized canvas arg's `rr_type` against the constructors the
recorder emits (`record/observers/canvas/serialize-args.ts`) instead of
resolving the name off `window`: the nine typed arrays, `DataView`,
`ImageData`, `ArrayBuffer` (normally serialized as base64, but reachable in
`args` form from older clients and nested inside `DataView` args), and `Array`
(the nested-arg wrapper older clients emit, covered by the existing
`preloadAllImages` tests).

Unknown types deserialize to `null` and log a warning, so a missing entry is
visible in the replayer console rather than a silently blank canvas. `null`
rather than a throw keeps one odd arg from taking down preload for an entire
session; the existing `warnCanvasMutationFailed` handler covers the downstream
failure.

`ImageBitmap`, `HTMLImageElement`, `Blob` and WebGL variable references are
unaffected -- those branches never reach the constructor path. No recorder
changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The allowlist from the previous commit now applies only when the new
`enforceCanvasArgAllowlist` player config option is set, so an embedding app
can roll it out — and turn it back off if a recording shape turns up that the
list doesn't cover — without shipping a new bundle. Defaults to false, which
leaves current behavior unchanged.

Threaded from `playerConfig` through the canvas mutation dispatcher into
`deserializeArg`, covering both the mutation path
(`replay/canvas/{index,2d,webgl}.ts`) and the eager preload path
(`deserializeAndPreloadCanvasEvents`). Also added to
`SERIALIZABLE_CONFIG_KEYS`, so the option survives the postMessage boundary
into the embedded replayer host; without that entry it would be silently
dropped for origin-isolated replay.

Recording is untouched — this is a replay-side option only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mayberryzane mayberryzane changed the title fix(replay): rebuild canvas args only from known constructors fix(replay): rebuild canvas args only from known constructors, behind a config option Aug 18, 2026
@mayberryzane
mayberryzane marked this pull request as ready for review August 18, 2026 20:16
@mayberryzane
mayberryzane merged commit 80db9d9 into main Aug 18, 2026
14 checks passed
@mayberryzane
mayberryzane deleted the canvas-arg-constructor-allowlist branch August 18, 2026 20:47
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.

2 participants