From 670ef5f7a374d43ced51f5f15e768944b16255bf Mon Sep 17 00:00:00 2001 From: Dhairyashil Date: Fri, 3 Jul 2026 01:26:57 +0530 Subject: [PATCH 1/3] fix(extension): harden booking URL handling --- .../extension/entrypoints/background/index.ts | 15 ++ apps/extension/entrypoints/content.ts | 174 +++++++----------- apps/extension/lib/linkedin.ts | 7 +- apps/extension/lib/utils.test.ts | 82 +++++++++ apps/extension/lib/utils.ts | 94 ++++++++++ apps/extension/package.json | 4 +- bun.lock | 1 + 7 files changed, 262 insertions(+), 115 deletions(-) create mode 100644 apps/extension/lib/utils.test.ts diff --git a/apps/extension/entrypoints/background/index.ts b/apps/extension/entrypoints/background/index.ts index e8eed4a9..2836a07d 100644 --- a/apps/extension/entrypoints/background/index.ts +++ b/apps/extension/entrypoints/background/index.ts @@ -1,5 +1,9 @@ /// +import { + validateExtensionOAuthAuthorizeUrl, + validateExtensionOAuthTokenEndpoint, +} from "../../lib/utils"; import type { Booking } from "../../types/bookings.types"; import type { OAuthTokens } from "../../types/oauth"; @@ -378,6 +382,12 @@ export default defineBackground(() => { if (message.action === "start-extension-oauth") { const authUrl = message.authUrl as string; + const expectedRedirectUrl = getIdentityAPI()?.getRedirectURL?.(); + const authUrlValidation = validateExtensionOAuthAuthorizeUrl(authUrl, expectedRedirectUrl); + if (!authUrlValidation.ok) { + sendResponse({ success: false, error: authUrlValidation.reason }); + return true; + } const state = new URL(authUrl).searchParams.get("state"); // Store state before starting OAuth to prevent race conditions @@ -781,6 +791,11 @@ async function handleTokenExchange( tokenEndpoint: string, state?: string ): Promise { + const tokenEndpointValidation = validateExtensionOAuthTokenEndpoint(tokenEndpoint); + if (!tokenEndpointValidation.ok) { + throw new Error(tokenEndpointValidation.reason); + } + if (state) { await validateOAuthState(state); } diff --git a/apps/extension/entrypoints/content.ts b/apps/extension/entrypoints/content.ts index e1abcc94..346c2460 100644 --- a/apps/extension/entrypoints/content.ts +++ b/apps/extension/entrypoints/content.ts @@ -1,7 +1,7 @@ /// import { initGoogleCalendarIntegration } from "../lib/google-calendar"; import { initLinkedInIntegration } from "../lib/linkedin"; -import { escapeHtml } from "../lib/utils"; +import { buildSafeBookingUrl, escapeHtml } from "../lib/utils"; /** * Development-only logging utility for content scripts. @@ -88,6 +88,10 @@ export default defineContentScript({ iframeContainer.appendChild(iframe); + function postToCompanionIframe(iframeWindow: Window | null, message: unknown): void { + iframeWindow?.postMessage(message, new URL(iframe.src).origin); + } + // Listen for messages from iframe to control width and handle OAuth window.addEventListener("message", (event) => { if (!iframeLoaded) return; @@ -188,36 +192,27 @@ export default defineContentScript({ "Failed to communicate with background script:", chrome.runtime.lastError.message ); - iframeWindow?.postMessage( - { - type: "cal-extension-oauth-result", - success: false, - error: `Extension communication failed: ${chrome.runtime.lastError.message}`, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-oauth-result", + success: false, + error: `Extension communication failed: ${chrome.runtime.lastError.message}`, + }); return; } // Forward the response back to iframe if (response.success) { - iframeWindow?.postMessage( - { - type: "cal-extension-oauth-result", - success: true, - responseUrl: response.responseUrl, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-oauth-result", + success: true, + responseUrl: response.responseUrl, + }); } else { - iframeWindow?.postMessage( - { - type: "cal-extension-oauth-result", - success: false, - error: response.error || "OAuth flow failed", - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-oauth-result", + success: false, + error: response.error || "OAuth flow failed", + }); } } ); @@ -244,36 +239,27 @@ export default defineContentScript({ "Failed to communicate with background script:", chrome.runtime.lastError.message ); - iframeWindow?.postMessage( - { - type: "cal-extension-token-exchange-result", - success: false, - error: `Extension communication failed: ${chrome.runtime.lastError.message}`, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-token-exchange-result", + success: false, + error: `Extension communication failed: ${chrome.runtime.lastError.message}`, + }); return; } // Forward the response back to iframe if (response.success) { - iframeWindow?.postMessage( - { - type: "cal-extension-token-exchange-result", - success: true, - tokens: response.tokens, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-token-exchange-result", + success: true, + tokens: response.tokens, + }); } else { - iframeWindow?.postMessage( - { - type: "cal-extension-token-exchange-result", - success: false, - error: response.error || "Token exchange failed", - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-token-exchange-result", + success: false, + error: response.error || "Token exchange failed", + }); } } ); @@ -290,25 +276,19 @@ export default defineContentScript({ "Failed to communicate with background script:", chrome.runtime.lastError.message ); - iframeWindow?.postMessage( - { - type: "cal-extension-sync-tokens-result", - success: false, - error: `Extension communication failed: ${chrome.runtime.lastError.message}`, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-sync-tokens-result", + success: false, + error: `Extension communication failed: ${chrome.runtime.lastError.message}`, + }); return; } - iframeWindow?.postMessage( - { - type: "cal-extension-sync-tokens-result", - success: response?.success ?? false, - error: response?.error, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-sync-tokens-result", + success: response?.success ?? false, + error: response?.error, + }); }); } @@ -319,25 +299,19 @@ export default defineContentScript({ "Failed to communicate with background script:", chrome.runtime.lastError.message ); - iframeWindow?.postMessage( - { - type: "cal-extension-clear-tokens-result", - success: false, - error: `Extension communication failed: ${chrome.runtime.lastError.message}`, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-clear-tokens-result", + success: false, + error: `Extension communication failed: ${chrome.runtime.lastError.message}`, + }); return; } - iframeWindow?.postMessage( - { - type: "cal-extension-clear-tokens-result", - success: response?.success ?? false, - error: response?.error, - }, - "*" - ); + postToCompanionIframe(iframeWindow, { + type: "cal-extension-clear-tokens-result", + success: response?.success ?? false, + error: response?.error, + }); }); } @@ -453,7 +427,7 @@ export default defineContentScript({ // Send message to iframe to reload cache if (iframe.contentWindow) { - iframe.contentWindow.postMessage({ type: "cal-companion-reload-cache" }, "*"); + postToCompanionIframe(iframe.contentWindow, { type: "cal-companion-reload-cache" }); } }); @@ -1089,11 +1063,7 @@ export default defineContentScript({ previewBtn.addEventListener("click", (e) => { e.stopPropagation(); - const bookingUrl = - eventType.bookingUrl || - `https://cal.com/${ - eventType.users?.[0]?.username || "user" - }/${eventType.slug}`; + const bookingUrl = buildSafeBookingUrl(eventType); window.open(bookingUrl, "_blank"); }); previewBtn.addEventListener("mouseenter", () => { @@ -1133,11 +1103,7 @@ export default defineContentScript({ copyBtn.addEventListener("click", (e) => { e.stopPropagation(); // Copy to clipboard - const bookingUrl = - eventType.bookingUrl || - `https://cal.com/${ - eventType.users?.[0]?.username || "user" - }/${eventType.slug}`; + const bookingUrl = buildSafeBookingUrl(eventType); navigator.clipboard .writeText(bookingUrl) .then(() => { @@ -1265,7 +1231,7 @@ export default defineContentScript({ Something went wrong
- ${errorMessage || "Failed to load event types"} + ${escapeHtml(errorMessage || "Failed to load event types")}