Skip to content

Replay player violates the app CSP on every replay #92057

Description

@pauldambra

Problem

The session replay player renders a recording into an about:blank iframe. Local-scheme frames inherit the embedder's Content Security Policy, so the replayer runs under the PostHog app's policy and its asset loads are judged against it. Replayed images, fonts, and stylesheets come from whatever origins the recorded site used, so they violate that policy on essentially every replay.

The app currently sends Content-Security-Policy-Report-Only, so nothing is blocked and the player works. These violations are the advance warning of what breaks the day the policy is enforced: images, most fonts, and some stylesheets stop loading in every replay.

Two defects sit behind it.

1. The replay asset proxy is not in the policy.

CorsPlugin rewrites replayed font and script URLs through https://replay.ph-proxy.com (common/replay-shared/src/rrweb-plugins/index.ts), so those loads come from one origin we control. That origin appears nowhere in CSPMiddleware (posthog/middleware.py). Every proxied font load therefore violates font-src. The proxy exists to make this tractable and the policy does not know about it.

2. CorsPlugin does not rewrite images.

It handles STYLE, LINK, and SCRIPT nodes. Image elements are left alone, so replayed images load straight from the recorded site's origin and violate img-src. Images are the largest share of the violations by some way.

Why this needs fixing rather than filtering

The policy has already started absorbing recorded-site origins one at a time. 2a54418efe ("chore: add more csp sources, from testing", #39824) added https://assets.faircado.com and https://use.typekit.net to font-src. Neither string appears anywhere else in the codebase — no frontend code, no config, no docs — so they are not app dependencies.

That PR's review raised both halves of this and neither was answered. An automated reviewer asked whether those two origins were "legitimate requirements and not temporary testing artifacts". A human reviewer asked how the change affected replay, and the answer was a hope that frame-src https: would cover it. It does not: frame-src governs which frames a document may embed, and the replayer's frame is about:blank, which inherits the policy instead. So the question was the right one and the mechanism just isn't the one anybody expected.

Left alone, the policy accretes one recorded-site origin per investigation and still never covers the next recording.

Proposed fix

  • Add https://replay.ph-proxy.com to img-src, font-src, style-src-elem, and media-src in CSPMiddleware.
  • Extend CorsPlugin to rewrite image URLs through the proxy, including srcset and CSS url() references in stylesheets.
  • Remove https://assets.faircado.com and https://use.typekit.net from font-src once proxied fonts are covered.

The tradeoff is proxy bandwidth: every replayed image would go through replay.ph-proxy.com rather than loading direct. Worth measuring before committing to it, and worth deciding what a proxy failure should degrade to.

Alternatives considered

Give the replayer iframe a real document. Only about:blank, srcdoc, blob: and data: inherit the embedder's policy. An iframe pointed at a real same-origin URL gets its own response headers instead. That would give the replay surface a self-identifying document URL and a policy scoped to it, without loosening the app's. It needs a change on the rrweb side, because Replayer constructs its own iframe.

Serve the player from a separate origin. Everything above, plus real origin isolation between replayed content and the app. The largest change, and it needs infrastructure work.

Tag the violations at ingestion and filter them out. Cheap and backend-only, but it hides the signal rather than fixing the policy, and does nothing about the enforcement cliff.

How this surfaced

Found while validating the origin filter in #91384, which drops CSP reports that legacy self-hosted installs send to Cloud. Checking what that filter would classify turned up this population instead: reports with about as the document URL, carrying our own middleware policy and Cloud static asset source files.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions