diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 35585d58863..452015a6fe1 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -53,6 +53,7 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; // eslint-disable-next-line no-restricted-imports import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { ExtensionUrlTokenService } from "../../platform/services/extension-url-token.service"; // FIXME (PM-22628): Popup imports are forbidden in background // eslint-disable-next-line no-restricted-imports import { @@ -236,6 +237,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private accountService: AccountService, private generatePasswordCallback: () => Promise, private addPasswordCallback: (password: string) => Promise, + private extensionUrlTokenService: ExtensionUrlTokenService, ) { this.initOverlayEventObservables(); } @@ -2947,7 +2949,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.postMessageToPort(port, { command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, - iframeUrl: chrome.runtime.getURL( + iframeUrl: this.extensionUrlTokenService.createTokenUrl( `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, ), pageTitle: chrome.i18n.getMessage( diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts index 663eae9144a..80fae91f887 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { validateExtensionUrl } from "@bitwarden/browser/platform/utils/extension-url-token.utils"; import { EVENTS } from "@bitwarden/common/autofill/constants"; import { setElementStyles } from "../../../../utils"; @@ -60,7 +61,14 @@ export class AutofillInlineMenuContainer { * * @param message - The message containing the iframe url and page title. */ - private handleInitInlineMenuIframe(message: InitAutofillInlineMenuElementMessage) { + private async handleInitInlineMenuIframe(message: InitAutofillInlineMenuElementMessage) { + const isValidUrl = await validateExtensionUrl(message.iframeUrl); + if (!isValidUrl) { + // eslint-disable-next-line no-console + console.error("Token not found"); + return; + } + this.defaultIframeAttributes.src = message.iframeUrl; this.defaultIframeAttributes.title = message.pageTitle; this.portName = message.portName; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3e6eac1f13a..eb6852face3 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -317,6 +317,7 @@ import BrowserInitialInstallService from "../platform/services/browser-initial-i import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; +import { ExtensionUrlTokenService } from "../platform/services/extension-url-token.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; @@ -452,6 +453,7 @@ export default class MainBackground { syncServiceListener: SyncServiceListener; browserInitialInstallService: BrowserInitialInstallService; backgroundSyncService: BackgroundSyncService; + extensionUrlTokenService: ExtensionUrlTokenService; webPushConnectionService: WorkerWebPushConnectionService | UnsupportedWebPushConnectionService; themeStateService: DefaultThemeStateService; @@ -1140,6 +1142,8 @@ export default class MainBackground { this.browserInitialInstallService = new BrowserInitialInstallService(this.stateProvider); + this.extensionUrlTokenService = new ExtensionUrlTokenService(); + if (BrowserApi.isManifestVersion(3)) { const registration = (self as unknown as { registration: ServiceWorkerRegistration }) ?.registration; @@ -1962,6 +1966,7 @@ export default class MainBackground { this.accountService, () => this.generatePassword(), (password) => this.addPasswordToHistory(password), + this.extensionUrlTokenService, ); this.autofillBadgeUpdaterService = new AutofillBadgeUpdaterService( diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index de0d79a89db..853e15a2935 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -80,6 +80,9 @@ export default class RuntimeBackground { BiometricsCommands.GetBiometricsStatusForUser, BiometricsCommands.CanEnableBiometricUnlock, "getUserPremiumStatus", + "createTokenExtensionUrl", + "validateExtensionUrl", + "revokeExtensionUrlToken", ]; if (messagesWithResponse.includes(msg.command)) { @@ -213,6 +216,16 @@ export default class RuntimeBackground { ); return result; } + case "createTokenExtensionUrl": { + return this.main.extensionUrlTokenService.createTokenUrl(msg.path); + } + case "validateExtensionUrl": { + return this.main.extensionUrlTokenService.validateUrl(msg.url); + } + case "revokeExtensionUrlToken": { + this.main.extensionUrlTokenService.revokeToken(msg.url); + return true; + } } } diff --git a/apps/browser/src/platform/services/abstractions/extension-url-token.service.ts b/apps/browser/src/platform/services/abstractions/extension-url-token.service.ts new file mode 100644 index 00000000000..52b6b92aed9 --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/extension-url-token.service.ts @@ -0,0 +1,11 @@ +export abstract class ExtensionUrlTokenService { + abstract generateToken(): string; + + abstract createTokenUrl(path: string): string; + + abstract validateUrl(url: string): boolean; + + abstract revokeToken(url: string): void; + + abstract extractToken(url: string): string | null; +} diff --git a/apps/browser/src/platform/services/extension-url-token.service.ts b/apps/browser/src/platform/services/extension-url-token.service.ts new file mode 100644 index 00000000000..22c242d704d --- /dev/null +++ b/apps/browser/src/platform/services/extension-url-token.service.ts @@ -0,0 +1,40 @@ +import { ExtensionUrlTokenService as ExtensionUrlTokenServiceAbstraction } from "./abstractions/extension-url-token.service"; + +export class ExtensionUrlTokenService implements ExtensionUrlTokenServiceAbstraction { + private validTokens = new Set(); + + generateToken(): string { + const token = crypto.randomUUID(); + this.validTokens.add(token); + return token; + } + + createTokenUrl(path: string): string { + const token = this.generateToken(); + const baseUrl = chrome.runtime.getURL(path); + const url = new URL(baseUrl); + url.searchParams.set("token", token); + return url.toString(); + } + + validateUrl(url: string): boolean { + const token = this.extractToken(url); + return token !== null && this.validTokens.has(token); + } + + revokeToken(url: string): void { + const token = this.extractToken(url); + if (token) { + this.validTokens.delete(token); + } + } + + extractToken(url: string): string | null { + try { + const urlObj = new URL(url); + return urlObj.searchParams.get("token"); + } catch { + return null; + } + } +} diff --git a/apps/browser/src/platform/utils/extension-url-token.utils.ts b/apps/browser/src/platform/utils/extension-url-token.utils.ts new file mode 100644 index 00000000000..5639aa1a459 --- /dev/null +++ b/apps/browser/src/platform/utils/extension-url-token.utils.ts @@ -0,0 +1,36 @@ +export async function createTokenExtensionUrl(path: string): Promise { + const response = await chrome.runtime.sendMessage({ + command: "createTokenExtensionUrl", + path: path, + }); + + if (response?.error) { + throw new Error(response.error.message || "Failed to create token URL"); + } + + return response.result; +} + +export async function validateExtensionUrl(url: string): Promise { + const response = await chrome.runtime.sendMessage({ + command: "validateExtensionUrl", + url: url, + }); + + if (response?.error) { + throw new Error(response.error.message || "Failed to validate URL"); + } + + return response.result; +} + +export async function revokeExtensionUrlToken(url: string): Promise { + const response = await chrome.runtime.sendMessage({ + command: "revokeExtensionUrlToken", + url: url, + }); + + if (response?.error) { + throw new Error(response.error.message || "Failed to revoke token"); + } +}