diff --git a/browser-extension/common/src/types.ts b/browser-extension/common/src/types.ts index 443b6c54a6..5cfe532fb1 100644 --- a/browser-extension/common/src/types.ts +++ b/browser-extension/common/src/types.ts @@ -113,6 +113,11 @@ export interface SessionRecordingConfig { widgetPosition?: { top?: number; bottom?: number; left?: number; right?: number }; recordingStartTime?: number; previousSession?: any; + // Per-recording relay auth (RQ-3095 / RQ-3096): a shared secret token and the + // top-frame origin, threaded to every frame so cross-origin iframe relay is + // authenticated (token) and not broadcast to untrusted frames (trustedOrigin). + relayToken?: string; + trustedOrigin?: string; } export interface RulePair { diff --git a/browser-extension/mv3/src/content-scripts/common/sessionRecorder.ts b/browser-extension/mv3/src/content-scripts/common/sessionRecorder.ts index 49facd6598..5ae8b1d89f 100644 --- a/browser-extension/mv3/src/content-scripts/common/sessionRecorder.ts +++ b/browser-extension/mv3/src/content-scripts/common/sessionRecorder.ts @@ -81,6 +81,8 @@ const sendStartRecordingEvent = async (sessionRecordingConfig: SessionRecordingC showWidget, widgetPosition, previousSession, + relayToken, + trustedOrigin, } = sessionRecordingConfig; const isIFrame = isIframe(); @@ -95,6 +97,10 @@ const sendStartRecordingEvent = async (sessionRecordingConfig: SessionRecordingC maxDuration: (sessionRecordingConfig.maxDuration || 5) * 60 * 1000, // minutes -> milliseconds previousSession: !isIFrame ? previousSession : null, localStorage: true, + // Relay auth: top frame uses relayToken to validate incoming events; iframes use + // both to authenticate and to target the relay at the top origin (RQ-3095/RQ-3096). + relayToken, + trustedOrigin, }); sessionRecorderState.isExplicitRecording = explicit; diff --git a/browser-extension/mv3/src/service-worker/services/sessionRecording.ts b/browser-extension/mv3/src/service-worker/services/sessionRecording.ts index d46c5ae942..359b7dbc02 100644 --- a/browser-extension/mv3/src/service-worker/services/sessionRecording.ts +++ b/browser-extension/mv3/src/service-worker/services/sessionRecording.ts @@ -10,6 +10,24 @@ import { isExtensionEnabled } from "../../utils"; const CONFIG_STORAGE_KEY = "sessionRecordingConfig"; +const getTopFrameOrigin = (url?: string): string | undefined => { + try { + return url ? new URL(url).origin : undefined; + } catch { + return undefined; + } +}; + +// Ensures the per-recording relay auth fields (RQ-3095 / RQ-3096) are present and +// stable across every frame of the tab: one random token, generated once and reused +// (so top and iframes share it), plus the top-frame origin used as the relay +// postMessage targetOrigin. Both are broadcast to all frames via startRecording. +const withRelayAuth = (data: Record, topUrl?: string): Record => ({ + ...data, + relayToken: data.relayToken ?? crypto.randomUUID(), + trustedOrigin: getTopFrameOrigin(topUrl), +}); + const getSessionRecordingConfig = async (url: string): Promise => { const sessionRecordingConfig = await getRecord(CONFIG_STORAGE_KEY); @@ -100,7 +118,7 @@ export const startRecordingExplicitly = async (tab: chrome.tabs.Tab, showWidget: return; } - const sessionRecordingData = { explicit: true, showWidget }; + const sessionRecordingData = withRelayAuth({ explicit: true, showWidget }, tab.url); tabService.setData(tab.id, TAB_SERVICE_DATA.SESSION_RECORDING, sessionRecordingData); startRecording(tab.id, sessionRecordingData); @@ -145,6 +163,11 @@ export const handleSessionRecordingOnClientPageLoad = async (tab: chrome.tabs.Ta } if (sessionRecordingData) { + // Attach/keep the stable relay token + top origin before broadcasting so every + // frame (top and iframes, including ones that load later) receives the same values. + sessionRecordingData = withRelayAuth(sessionRecordingData, tab.url); + tabService.setData(tab.id, TAB_SERVICE_DATA.SESSION_RECORDING, sessionRecordingData); + startRecording(tab.id, sessionRecordingData).then(() => { tabService.setData(tab.id, TAB_SERVICE_DATA.SESSION_RECORDING, { ...sessionRecordingData,