From 2522dbd8049d8b89c1c7a15b5711f7d64f82ea89 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:38:57 +0200 Subject: [PATCH] fix: keep the plugin denylist and unit tests from breaking Lite and Docker builds --- .dockerignore | 5 +++ .../plugin-http-post-denied-paths.test.ts | 6 +++ lib/plugin-sandbox/host-api.ts | 37 +++++++++++-------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.dockerignore b/.dockerignore index 6ba5f7e12..e3ba81e97 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,6 +14,11 @@ TODO.md !README.md # Sibling projects / test harness - not part of the webmail image examples/ +# Unit tests: `next build` type-checks every .ts file, and some tests import +# routes the Dockerfile deletes (app/api/dev-jmap) +**/__tests__ +**/*.test.ts +**/*.test.tsx integration/ repos/ # Runtime state of a checkout that has been run (admin password hash, diff --git a/lib/__tests__/plugin-http-post-denied-paths.test.ts b/lib/__tests__/plugin-http-post-denied-paths.test.ts index e11310b7e..b9d9cadda 100644 --- a/lib/__tests__/plugin-http-post-denied-paths.test.ts +++ b/lib/__tests__/plugin-http-post-denied-paths.test.ts @@ -51,6 +51,7 @@ describe('plugin http.post denied paths', () => { ['/api/account/stalwart/jmap', ['/api/account/stalwart/jmap']], ['/api/admin/config', ['/api/admin/']], ['/api/settings', ['/api/settings']], + ['/api/wopi', ['/api/wopi']], ['/api/auth/session', ['/api/']], ['/api/%61dmin/config', ['/api/%61dmin/']], ['/api//admin/config', ['/api/']], @@ -63,4 +64,9 @@ describe('plugin http.post denied paths', () => { await dispatchApiCall(plugin(['/api/translate']), 'http.post', ['/api/translate', { text: 'hi' }]); expect(fetchSpy).toHaveBeenCalledTimes(1); }); + + it('reaches a route that only shares a prefix with a denied one', async () => { + await dispatchApiCall(plugin(['/api/admin-helper']), 'http.post', ['/api/admin-helper', {}]); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index 64d6e4ae4..74cc159f3 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -293,22 +293,26 @@ function isApiPostPathAllowed(path: string, allowlist: readonly string[]): boole * lists: they act with the user's full credentials (the JMAP passthrough, * WebDAV, CalDAV), change sign-in, admin or plugin state, or belong to the * setup wizard. A plugin talks to its own sidecar routes instead. + * + * Listed by their segment under `/api/`, not as `/api/...` strings: the Lite + * build reads every such string in a client chunk as an endpoint the browser + * calls and refuses the ones it has no stand-in for (scripts/lite/verify.mjs). */ -const PLUGIN_POST_DENIED_PATHS = [ - '/api/account', - '/api/admin', - '/api/auth', - '/api/caldav', - '/api/dev-jmap', - '/api/plugin-approval-status', - '/api/plugins', - '/api/push', - '/api/settings', - '/api/setup', - '/api/system', - '/api/webdav', - '/api/wopi', -]; +const PLUGIN_POST_DENIED_ROUTES = new Set([ + 'account', + 'admin', + 'auth', + 'caldav', + 'dev-jmap', + 'plugin-approval-status', + 'plugins', + 'push', + 'settings', + 'setup', + 'system', + 'webdav', + 'wopi', +]); /** * Checked on the path the router will see: decoded once and with repeated @@ -322,7 +326,8 @@ function isPluginPostDenied(pathname: string): boolean { return true; } routePath = routePath.replace(/\/{2,}/g, '/'); - return PLUGIN_POST_DENIED_PATHS.some((denied) => routePath === denied || routePath.startsWith(`${denied}/`)); + if (!routePath.startsWith('/api/')) return false; + return PLUGIN_POST_DENIED_ROUTES.has(routePath.slice('/api/'.length).split('/')[0]); } interface PluginHttpPostOptions {