feat(replay): origin-isolated embedded replayer over postMessage (SEC-8885) - #37
Draft
mayberryzane wants to merge 3 commits into
Draft
feat(replay): origin-isolated embedded replayer over postMessage (SEC-8885)#37mayberryzane wants to merge 3 commits into
mayberryzane wants to merge 3 commits into
Conversation
…-8885) Add an embedded-replay bridge so the Replayer can run inside a sandboxed, cookieless, cross-origin iframe and be driven from the parent page, instead of running in the embedding app's privileged origin. - protocol.ts: typed, versioned, namespaced message contract. Data-only (no functions/DOM cross the boundary) and both sides authenticate their peer (host by event.origin, client by event.source identity). pickSerializableConfig allowlists the data-only playerConfig subset. - host.ts: EmbeddedReplayerHost runs inside the iframe, constructs the Replayer in that realm, forwards ReplayerEvents back, applies control commands, answers getter RPCs, and pushes current-time telemetry while playing. - client.ts: EmbeddedReplayerClient mirrors the Replayer control/event/getter surface the viewer needs, over the bridge. - host-document.ts: buildHostDocument emits the sandbox HTML shell with a <meta> CSP whose script-src omits unsafe-eval (kills the canvas Function() path even inside the sandbox). Purely additive; existing in-parent Replayer usage is unchanged. Unit tests cover the protocol guards, config sanitization, and the client bridge behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drop the request/response RPC layer. The viewer's stable getters (getMetaData, getActivityIntervals) now ride the `initialized` message (re-sent on replaceEvents) and current time streams via `time`, so the client's getters are synchronous cached reads. Removes pending-map/request-id bookkeeping on the client and reply() on the host. - Replace the rAF + manual-throttle time pump with a plain setInterval. - Reuse the package's playerMetaData / SessionInterval types instead of the parallel PlayerMetaDataLike; drop the SerializablePayload alias. - Require a configured parentOrigin (option or query param) instead of the trust-on-first-message fallback; host ignores messages when none is set. Command surface (resume/addEvent/enable/disableInteract) intentionally kept for now; it'll be trimmed to actual callers when the gonfalon integration lands. Typecheck + lint clean; 10 unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Foundation for origin isolation — option #2 in the Observability XSS cluster analysis. SEC-8885 is a zero-click stored XSS: the canvas deserializer's
new window[rr_type](...)runs in the host page's realm (theReplayerruns in the parent and only reaches into the child iframe), so a poisonedPromise(Function("code"))executes as first-party JS onapp.launchdarkly.com— full account takeover. The replayer<meta>CSP (#36) doesn't stop it because that CSP only governs the rebuilt-DOM child, not the parent realm where the deserializer runs.The durable fix is to run the
Replayerinside a sandboxed, cookieless, cross-origin iframe and drive it from the parent — so even if attacker JS executes, it runs somewhere with no app cookies, no same-origin API access, and no reach into the parent window. This PR adds the rrweb-side capability that makes that possible; the current published API can't, because it assumes parent-realm execution.What
Purely additive. Existing in-parent
Replayerusage is unchanged.protocol.ts— typed, versioned, namespacedpostMessagecontract. Data-only (no functions/DOM ever cross the boundary; nothing iseval'd or constructed by name from a message). Both sides authenticate their peer: the host byevent.origin(against a declared parent origin), the client byevent.sourceidentity (a sandboxed opaque-origin frame reportsevent.origin === "null", so source identity is the reliable check).pickSerializableConfigallowlists the data-onlyplayerConfigsubset.host.ts—EmbeddedReplayerHost/startEmbeddedReplayerHostrun inside the iframe: construct theReplayerin that realm, forwardReplayerEventsback, apply playback/config commands, answer getter RPCs, and push current-time telemetry while playing.client.ts—EmbeddedReplayerClientmirrors the control / event / getter surface the viewer needs, over the bridge.host-document.ts—buildHostDocumentemits the sandbox HTML shell with a<meta>CSP whosescript-srcomitsunsafe-eval— this kills thenew Function(...)canvas path even inside the sandbox (defense in depth on top of the origin isolation itself).Isolation without a new domain
An iframe with
sandbox="allow-scripts"withoutallow-same-originis forced into an opaque origin regardless of the URL it loads from — cookieless and cross-site to the app. That defeats the SEC-8885 PoC as a code-only change (no DNS/TLS/hosting). A dedicated cross-site domain remains a stronger follow-up (preserves same-origin among the replay's own nested frames, enables a real HTTP-header CSP), and the host origin is a config value so promoting later is a swap, not a rewrite.Validation
tsc -noEmitclean,eslintclean.test/replay/embedded.test.ts): protocol guards, config sanitization (drops functions/DOM/unknown keys), client command/event/RPC round-trip, and untrusted-source rejection.Replayer, and nested-iframe replay fidelity under isolation.Downstream
gonfalon consumes this as
@highlight-run/rrweb. After merge: publish2.0.0-lambda.8→ bump the dep in gonfalon → gonfalon integration PR wiresEmbeddedReplayerClientinto the Sessions viewer behind a flag.🤖 Generated with Claude Code