From cfdb3bfa9e65ad423e6c5dd9da13baa6887bc1e6 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:56:53 +0530 Subject: [PATCH] fix: correctly handle iframe `allow` attribute --- package.json | 1 + pnpm-lock.yaml | 10 ++++ src/background/services/background.ts | 8 +++ src/background/services/monetization.ts | 28 +++++++++- .../__tests__/monetizationLinkManager.test.ts | 1 + src/content/services/frameManager.ts | 55 ++++++++++++++++--- src/shared/messages.ts | 9 +++ 7 files changed, 104 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index c050c8d5e..325d51eae 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "httpbis-digest-headers": "^1.0.0", "iso8601-duration": "^2.1.2", "loglevel": "^1.9.2", + "permissions-policy-allows-feature": "^0.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", "safe-buffer": "5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fffa91277..772680ea6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: loglevel: specifier: ^1.9.2 version: 1.9.2 + permissions-policy-allows-feature: + specifier: ^0.0.1 + version: 0.0.1 react: specifier: ^19.0.0 version: 19.0.0 @@ -3013,6 +3016,9 @@ packages: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} + permissions-policy-allows-feature@0.0.1: + resolution: {integrity: sha512-e4jKNNwFFzzY4ypI6d/ZdcOoTZUQxDP+U2vnFCVZrTIPKTFqQQfDaDbQqdkgayZiafpV0CJ3NjeN5Fw6MvJYqw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -6942,6 +6948,10 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 + permissions-policy-allows-feature@0.0.1: + dependencies: + structured-headers: 1.0.1 + picocolors@1.1.1: {} picomatch@2.3.1: {} diff --git a/src/background/services/background.ts b/src/background/services/background.ts index 87165f8c6..55a75b75e 100644 --- a/src/background/services/background.ts +++ b/src/background/services/background.ts @@ -314,6 +314,14 @@ export class Background { ); return; + case 'IS_MONETIZATION_ALLOWED': + return success( + this.monetizationService.isMonetizationAllowed( + message.payload, + sender, + ), + ); + // endregion // region App diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index 90a6582d2..d362ba38b 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -1,5 +1,6 @@ import type { Runtime, Tabs } from 'webextension-polyfill'; import type { + IsMonetizationAllowedPayload, PayWebsitePayload, PayWebsiteResponse, ResumeMonetizationPayload, @@ -7,7 +8,7 @@ import type { StopMonetizationPayload, } from '@/shared/messages'; import { PaymentSession } from './paymentSession'; -import { computeRate, getSender, getTabId } from '@/background/utils'; +import { computeRate, getSender, getTab, getTabId } from '@/background/utils'; import { isOutOfBalanceError } from './openPayments'; import { OUTGOING_PAYMENT_POLLING_MAX_ATTEMPTS, @@ -21,6 +22,7 @@ import { removeQueryParams, transformBalance, } from '@/shared/helpers'; +import { PermissionsPolicy } from 'permissions-policy-allows-feature'; import type { AmountValue, PopupStore, Storage } from '@/shared/types'; import type { OutgoingPayment } from '@interledger/open-payments'; import type { Cradle } from '@/background/container'; @@ -550,6 +552,30 @@ export class MonetizationService { }; } + isMonetizationAllowed( + payload: IsMonetizationAllowedPayload, + sender: Runtime.MessageSender, + ): boolean { + const tab = getTab(sender); + + // This will be stored in tabState likely + // https://github.com/interledger/web-monetization-extension/issues/959 + // We also want to allow/disallow monetization on host-document in future with this. + const permissionsPolicy = new PermissionsPolicy({ + headerValue: '', // we'll get this via webRequest in future + origin: tab.url, + defaultAllowlist: { monetization: "'self'" }, + }); + + // get the permissions policy for given iframe + const { allowAttribute: allow, origin } = payload; + const iframePermissionsPolicy = permissionsPolicy.inherit({ + allow, + origin, + }); + return iframePermissionsPolicy.allowsFeature('monetization', origin); + } + private async adjustSessionsAmount( sessions: PaymentSession[], rate: AmountValue, diff --git a/src/content/__tests__/monetizationLinkManager.test.ts b/src/content/__tests__/monetizationLinkManager.test.ts index e62ba622b..b113a4efe 100644 --- a/src/content/__tests__/monetizationLinkManager.test.ts +++ b/src/content/__tests__/monetizationLinkManager.test.ts @@ -61,6 +61,7 @@ const msg: MessageMocks = { START_MONETIZATION: jest.fn(), STOP_MONETIZATION: jest.fn(), TAB_FOCUSED: jest.fn(), + IS_MONETIZATION_ALLOWED: jest.fn(), }; const messageMock = jest.spyOn(messageManager, 'send'); // @ts-expect-error let it go diff --git a/src/content/services/frameManager.ts b/src/content/services/frameManager.ts index 2ad357947..62b0d4658 100644 --- a/src/content/services/frameManager.ts +++ b/src/content/services/frameManager.ts @@ -18,7 +18,12 @@ export class FrameManager { private frameAllowAttrObserver: MutationObserver; private frames = new Map< HTMLIFrameElement, - { frameId: string | null; requestIds: string[] } + { + frameId: string | null; + requestIds: string[]; + allowAttribute: string; + origin: string; + } >(); constructor({ window, document, logger, message }: Cradle) { @@ -78,9 +83,21 @@ export class FrameManager { } const hasTarget = this.frames.has(target); const typeSpecified = - target instanceof HTMLIFrameElement && target.allow === 'monetization'; + target instanceof HTMLIFrameElement && + target.allow.includes('monetization'); + let allowedByPermissionsPolicy = false; if (!hasTarget && typeSpecified) { + const res = await this.message.send('IS_MONETIZATION_ALLOWED', { + allowAttribute: target.allow, + origin: new URL(target.src, location.origin).origin, + }); + if (res.success && res.payload) { + allowedByPermissionsPolicy = true; + } + } + + if (!hasTarget && typeSpecified && allowedByPermissionsPolicy) { await this.onAddedFrame(target); handledTags.add(target); } else if (hasTarget && !typeSpecified) { @@ -97,6 +114,8 @@ export class FrameManager { this.frames.set(frame, { frameId: null, requestIds: [], + allowAttribute: frame.allow, + origin: new URL(frame.src, location.origin).origin, }); } @@ -181,7 +200,7 @@ export class FrameManager { private bindMessageHandler() { this.window.addEventListener( 'message', - (event: MessageEvent) => { + async (event: MessageEvent) => { const { message, payload, id } = event.data; if (!HANDLED_MESSAGES.includes(message)) { return; @@ -201,15 +220,26 @@ export class FrameManager { this.frames.set(frame, { frameId: id, requestIds: [], + allowAttribute: frame.allow, + origin: new URL(frame.src, location.origin).origin, }); return; - case 'IS_MONETIZATION_ALLOWED_ON_START': + case 'IS_MONETIZATION_ALLOWED_ON_START': { event.stopPropagation(); - if (frame.allow === 'monetization') { + const permissionsPolicyPayload = { + allowAttribute: frame.allow, + origin: new URL(frame.src, location.origin).origin, + }; + const res = await this.message.send( + 'IS_MONETIZATION_ALLOWED', + permissionsPolicyPayload, + ); + if (res.success && res.payload) { this.frames.set(frame, { frameId: id, requestIds: payload.map((p) => p.requestId), + ...permissionsPolicyPayload, }); eventSource.postMessage( { message: 'START_MONETIZATION', id, payload }, @@ -218,13 +248,23 @@ export class FrameManager { } return; + } - case 'IS_MONETIZATION_ALLOWED_ON_RESUME': + case 'IS_MONETIZATION_ALLOWED_ON_RESUME': { event.stopPropagation(); - if (frame.allow === 'monetization') { + const permissionsPolicyPayload = { + allowAttribute: frame.allow, + origin: new URL(frame.src, location.origin).origin, + }; + const res = await this.message.send( + 'IS_MONETIZATION_ALLOWED', + permissionsPolicyPayload, + ); + if (res.success && res.payload) { this.frames.set(frame, { frameId: id, requestIds: payload.map((p) => p.requestId), + ...permissionsPolicyPayload, }); eventSource.postMessage( { message: 'RESUME_MONETIZATION', id, payload }, @@ -232,6 +272,7 @@ export class FrameManager { ); } return; + } default: return; diff --git a/src/shared/messages.ts b/src/shared/messages.ts index c13dd1530..2afa375f2 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -193,6 +193,11 @@ export type StopMonetizationPayload = StopMonetizationPayloadEntry[]; export type ResumeMonetizationPayload = StartMonetizationPayload; +export interface IsMonetizationAllowedPayload { + allowAttribute: string; + origin: string; +} + export interface IsTabMonetizedPayload { value: boolean; } @@ -218,6 +223,10 @@ export type ContentToBackgroundMessage = { input: ResumeMonetizationPayload; output: never; }; + IS_MONETIZATION_ALLOWED: { + input: IsMonetizationAllowedPayload; + output: boolean; + }; }; // #endregion