Files
webmail/lib/__tests__/plugin-post-denylist.test.ts
T
Linus Rath 51c09a1a69 fix: never let a notification's buttons go round plugins, session limits or Trash
Review of the notification actions. With a plugin that may stop a move
or read change, or a silent tab holding the login, the message now
opens instead of the route acting. The route answers 401 under a
session limit, and neither path touches a message since moved to Trash
or Junk. A tab that thawed declines an old click, Archive follows the
gate of the notification's login and files into a shared-out Archive
as the app's button does, Reply opens a composer in the Pro shell, the
route retries once when another client made the folder first and
leaves no empty folder behind, and logins match however the server
address is written.

Two assertions of this feature's own tests change: the route answers
401, not 403, when a login's scopes can't be worked out, since that now
also covers session limits; a tab's message to the page now carries
`at`.

The Pro shell's mail surface now reads a parked link's query from the
handoff: the address bar may have none by its first render, which lost
`?slot=` and `?owner=` there too.
2026-11-03 06:28:04 +01:00

50 lines
1.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { sanitizeApiPostPaths } from '@/lib/extension-origins';
vi.mock('@/stores/toast-store', () => ({
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() },
}));
// host-api pulls in the whole store layer; only the pure gate is used here.
import { isPluginPostDenied } from '@/lib/plugin-sandbox/host-api';
// A plugin with `http:post` may POST to the same-origin routes its manifest
// lists, with the user's cookies. The denylist kept it off the passthrough,
// admin and sign-in routes but not off the plugin proxy and secrets routes:
// `/api/plugin-proxy?plugin=<other>&credential=<id>` injects another
// plugin's connected account or admin key into a request and hands the
// answer back, and `/api/plugin-secrets` re-points or deletes them. The
// agenda, fetch-ical and translate routes gave data and reach the plugin's
// own permissions would not. A manifest entry of the bare `/api/` prefix
// also passed validation and allowed every route at once.
describe('routes no plugin may POST to', () => {
it.each([
'/api/plugin-proxy',
'/api/plugin-secrets',
'/api/calendar-agenda',
'/api/fetch-ical',
'/api/translate',
'/api/account-archive',
'/api/%70lugin-proxy',
'/api//plugin-secrets',
'/api/account/stalwart/jmap',
'/api/push/action',
])('%s is denied', (path) => {
expect(isPluginPostDenied(path)).toBe(true);
});
it('leaves a plugin sidecar route alone', () => {
expect(isPluginPostDenied('/api/my-plugin/webhook')).toBe(false);
});
});
describe('apiPostPaths manifest entries', () => {
it('rejects the bare API prefix and keeps named routes', () => {
expect(sanitizeApiPostPaths(['/api/', '/api//x', '/api/my-plugin/', '/api/my-plugin/hook'])).toEqual([
'/api/my-plugin/',
'/api/my-plugin/hook',
]);
});
});