diff --git a/apps/extension/entrypoints/background/index.ts b/apps/extension/entrypoints/background/index.ts index e8eed4a9..e57fcedf 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"; @@ -7,6 +11,47 @@ const DEV_API_KEY = import.meta.env.EXPO_PUBLIC_CAL_API_KEY as string | undefine const IS_DEV_MODE = Boolean(DEV_API_KEY && DEV_API_KEY.length > 0); const BROWSER_TARGET = import.meta.env.BROWSER_TARGET || "chrome"; +function compactRedirectUrls(urls: Array): string[] { + return urls.filter((url): url is string => Boolean(url?.trim())); +} + +function getConfiguredOAuthRedirectUrls(): string[] { + const defaultRedirectUrls = compactRedirectUrls([ + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI, + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EU, + ]); + + switch (BROWSER_TARGET) { + case "firefox": + return compactRedirectUrls([ + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX, + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_FIREFOX_EU, + ...defaultRedirectUrls, + ]); + case "safari": + return compactRedirectUrls([ + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI, + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_SAFARI_EU, + ...defaultRedirectUrls, + ]); + case "edge": + return compactRedirectUrls([ + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE, + import.meta.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE_EU, + ...defaultRedirectUrls, + ]); + default: + return defaultRedirectUrls; + } +} + +function getExpectedOAuthRedirectUrls(): string[] { + return compactRedirectUrls([ + getIdentityAPI()?.getRedirectURL?.(), + ...getConfiguredOAuthRedirectUrls(), + ]); +} + const devLog = { log: (...args: unknown[]) => IS_DEV_MODE && console.log("[Cal.com]", ...args), warn: (...args: unknown[]) => IS_DEV_MODE && console.warn("[Cal.com]", ...args), @@ -378,6 +423,12 @@ export default defineBackground(() => { if (message.action === "start-extension-oauth") { const authUrl = message.authUrl as string; + const expectedRedirectUrls = getExpectedOAuthRedirectUrls(); + const authUrlValidation = validateExtensionOAuthAuthorizeUrl(authUrl, expectedRedirectUrls); + 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 @@ -555,7 +606,7 @@ async function handleExtensionOAuth(authUrl: string): Promise { devLog.log("Expected redirect URL:", expectedRedirectUrl); devLog.log("Auth URL redirect_uri:", redirectUri); - if (redirectUri && !redirectUri.startsWith(expectedRedirectUrl.replace(/\/$/, ""))) { + if (redirectUri && !validateExtensionOAuthAuthorizeUrl(authUrl, expectedRedirectUrl).ok) { devLog.warn( "MISMATCH! redirect_uri does not match expected URL.", "\nExpected:", @@ -781,6 +832,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..85abf233 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")}