From cef2438c3356fa2b2a0aee64d71fb05cfd72e22d Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:34:31 +0200 Subject: [PATCH] fix: let untrusted plugins upload attachments for the composer again --- lib/__tests__/plugin-api-gates.test.ts | 11 ++++++++--- lib/plugin-sandbox/host-api.ts | 6 +++--- lib/plugin-sandbox/runtime.tsx | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/__tests__/plugin-api-gates.test.ts b/lib/__tests__/plugin-api-gates.test.ts index 27a895eb6..d33b80ad1 100644 --- a/lib/__tests__/plugin-api-gates.test.ts +++ b/lib/__tests__/plugin-api-gates.test.ts @@ -306,11 +306,16 @@ describe('native keyword extension API', () => { }); }); -describe('privileged-only methods', () => { - it('refuses jmap.uploadBlob to an untrusted plugin even with email:blob-write', async () => { +describe('jmap.uploadBlob', () => { + it('is open to an untrusted plugin with email:blob-write, for composer attachment sources', async () => { const p = plugin({ permissions: ['email:blob-write'], grantedPermissions: ['email:blob-write'] }); expect(await gateError(p, 'jmap.uploadBlob', [new Uint8Array(1), 'a.bin', 'application/octet-stream'])) - .toMatch(/requires the privileged plugin tier/); + .not.toMatch(GATE_FAILURE); + }); + + it('still needs email:blob-write', async () => { + expect(await gateError(plugin(), 'jmap.uploadBlob', [new Uint8Array(1), 'a.bin', 'x/y'])) + .toMatch(/lacks permission "email:blob-write"/); }); }); diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index 19650ca89..b421f94f6 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -40,9 +40,9 @@ import { EncryptionAtRestConfig, PublicKeyInfo, PublicKeyInput, useAccountSecuri */ const PRIVILEGED_ONLY_METHODS = new Set([ 'jmap.fetchBlob', - // Puts arbitrary bytes on the mail server. Untrusted plugins attach files - // through the composer and draft APIs instead, which bound and label them. - 'jmap.uploadBlob', + // NOTE: jmap.uploadBlob is deliberately NOT here. The composer's + // `composer-attachment-source` slot is built for untrusted plugins that + // upload a file (from cloud storage, say) and hand the blob id to onAttach. 'jmap.sendRaw', 'jmap.submitRaw', 'jmap.importRaw', diff --git a/lib/plugin-sandbox/runtime.tsx b/lib/plugin-sandbox/runtime.tsx index 20f02b1ef..ffda72e4f 100644 --- a/lib/plugin-sandbox/runtime.tsx +++ b/lib/plugin-sandbox/runtime.tsx @@ -328,6 +328,7 @@ function buildPluginApi(manifest: PluginManifest) { /** Fetch a blob's raw bytes by id. Resolves to a Uint8Array. */ fetchBlob: (blobId: string, opts?: { name?: string; type?: string, rangeHeader?: number }) => callApi('jmap.fetchBlob', [blobId, opts]) as Promise, + /** Upload bytes to the mail server (email:blob-write); pass the blob id to a composer's onAttach. */ uploadBlob: (content: Uint8Array, name: string, type: string) => callApi('jmap.uploadBlob', [content, name, type]) as Promise<{ blobId: string; size: number; type: string; }>, /** Submit a fully-formed raw RFC822 message (already signed/encrypted). */