Skip to content

fix(csp): let recordings play on the site, not just in the embed - #84

Merged
ralyodio merged 1 commit into
masterfrom
worktree-fix-replay-csp
Aug 29, 2026
Merged

fix(csp): let recordings play on the site, not just in the embed#84
ralyodio merged 1 commit into
masterfrom
worktree-fix-replay-csp

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The bug

Past lives don't play anywhere on pairux.com — /l/<code>, reached from /channels → a channel → its history. Reported on https://pairux.com/l/TER8XG.

Cause

media-src in apps/web/src/middleware.ts was widened to https: only for /embed/*:

embeddable ? "media-src 'self' blob: https:" : "media-src 'self' blob:",

Recordings are streamed from Supabase Storage (https://<ref>.supabase.co/storage/v1/object/public/recordings/…), a cross-origin URL. So the same MP4 that plays in the embeddable player is blocked on the site's own pages.

Nothing else was wrong: the <video> renders with the correct src, and the file serves fine (200, video/mp4, 21.9 MB, range support). Chrome just refuses the load, and says so only in the console:

Loading media from 'https://…/1787936661676.mp4' violates the following
Content Security Policy directive: "media-src 'self' blob:".
MEDIA_ELEMENT_ERROR: Media load rejected by URL safety check

That is why it reads as "the player is broken" rather than as a policy error.

The fix

One media-src for every page, allowing the storage origin, derived from the same NEXT_PUBLIC_SUPABASE_URL that builds the playback URLs — with the project wildcard as a fallback so a missing var can't silently re-break replay. This is narrower than what /embed/* had: a specific origin instead of all https:. frame-ancestors keeps its embed-only relaxation.

Verification

Against prod /l/TER8XG in headless Chrome:

readyState error plays
before 0 code 4, "Media load rejected by URL safety check" no
after (patched CSP) 4 none yes — 126.6s, 1280x720, currentTime advances

Plus vitest run src/middleware.test.ts (11 passed), tsc --noEmit clean, eslint clean.

Also adds the first CSP tests for this middleware — there were none, which is how the embed/site divergence went unnoticed. One of them asserts the two surfaces agree on media-src.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WFLwZtXE6iBJyJSgrsWqRM

media-src only allowed a remote origin on /embed/*, so every past live was
CSP-blocked on pairux.com's own replay pages. The <video> rendered with the
right src and the MP4 served fine — Chrome just refused the load with
"MEDIA_ELEMENT_ERROR: Media load rejected by URL safety check", visible
only in the console, so the player looked broken for no reason.

Allow the Supabase Storage origin on every page instead, derived from the
same NEXT_PUBLIC_SUPABASE_URL that builds the playback URLs. This is also
narrower than what /embed/* had: a specific origin rather than all https:.

Verified against prod /l/TER8XG in headless Chrome — blocked before the
change; after it the recording loads (126.6s, 1280x720) and plays with no
CSP violations. Adds the first CSP tests for the middleware, covering the
media-src/embed divergence that let this through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFLwZtXE6iBJyJSgrsWqRM
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

67 finding(s)

HIGH/CRITICAL: 13 | MEDIUM: 36 | LOW: 18

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH js-electron-node-integration apps/desktop/src/main/window.ts:49
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:691
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:715
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:813
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:815
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:894
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:910
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1064
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1066
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1128
HIGH sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
HIGH sh-remote-script-execution apps/turn/deploy-droplet.sh:62
MEDIUM insecure-temp-file .githooks/commit-msg:19
MEDIUM insecure-temp-file .githooks/post-commit:22
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:370
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:393
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:402
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:438
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:48
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:192
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:125
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:219
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:142
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:145
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:122
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:284
MEDIUM js-open-redirect apps/web/src/app/pricing/UpgradeButton.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:282
MEDIUM js-open-redirect apps/web/src/hooks/useDesktopHandoff.ts:24
MEDIUM redos-nested-quantifier apps/web/src/lib/deliverable.ts:11
MEDIUM manifest-install-lifecycle-script package.json:55
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:208
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:313
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:342
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:264
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:288
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:201
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:261
MEDIUM js-shell-exec-interpolation scripts/release.mjs:145
MEDIUM js-shell-exec-interpolation scripts/release.mjs:146
MEDIUM js-shell-exec-interpolation scripts/release.mjs:147
MEDIUM js-shell-exec-interpolation scripts/version-bump.mjs:169
MEDIUM js-shell-exec-interpolation scripts/version-bump.mjs:170
MEDIUM js-shell-exec-interpolation scripts/version-bump.mjs:171
LOW insecure-temp-file apps/desktop/src/main/streaming/index.test.ts:64

…and 17 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 53c9a96 into master Aug 29, 2026
13 checks passed
@ralyodio
ralyodio deleted the worktree-fix-replay-csp branch August 29, 2026 08:43
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