fix: refuse plugin approval requests while user plugins or their review are turned off (GHSA-qc44-275p-gqw9)

This commit is contained in:
Linus Rath
2026-10-26 11:29:36 +01:00
parent b5c6b67ba4
commit cb51847ab3
2 changed files with 29 additions and 0 deletions
@@ -11,6 +11,11 @@ vi.mock('@/lib/stalwart/auth-context', () => ({
}));
vi.mock('@/lib/security/same-origin', () => ({ rejectCrossOriginRequest: () => null }));
let features: Record<string, boolean> = {};
vi.mock('@/lib/admin/config-manager', () => ({
configManager: { ensureLoaded: async () => {}, getPolicy: () => ({ features }) },
}));
let currentUser = 'alice@example.org';
const entries: Array<Record<string, unknown>> = [];
const bundles = new Map<string, Uint8Array>();
@@ -55,6 +60,7 @@ beforeEach(() => {
entries.length = 0;
bundles.clear();
currentUser = 'alice@example.org';
features = { pluginsEnabled: true, pluginsUploadEnabled: true, requirePluginApproval: true };
});
describe('POST /api/plugin-approval-status', () => {
@@ -87,6 +93,19 @@ describe('POST /api/plugin-approval-status', () => {
expect(entries).toHaveLength(0);
});
it.each([
['plugins are off', { pluginsEnabled: false }],
['user plugins are off', { pluginsUploadEnabled: false }],
['approval is not required', { requirePluginApproval: false }],
])('takes no package while %s', async (_label, off) => {
features = { ...features, ...off };
const res = await submit(await zipOf(MANIFEST));
expect(res.status).toBe(403);
expect(entries).toHaveLength(0);
expect(bundles.size).toBe(0);
});
it('limits how many requests one user can have open', async () => {
for (let i = 0; i < 10; i++) {
expect((await submit(await zipOf({ ...MANIFEST, id: `p-${i}x` }))).status).toBe(200);
+10
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { rejectCrossOriginRequest } from '@/lib/security/same-origin';
import { configManager } from '@/lib/admin/config-manager';
import { cookies } from 'next/headers';
import { decryptSession } from '@/lib/auth/crypto';
import { sessionCookieName } from '@/lib/auth/session-cookie';
@@ -75,6 +76,15 @@ export async function POST(request: NextRequest) {
const username = await resolveUsername();
if (!username) return NextResponse.json({ error: 'unauthenticated' }, { status: 401 });
// Only take packages while users may install their own plugins and
// those need review (GHSA-qc44-275p-gqw9): otherwise nobody asked for
// the queue, and every request still stores a ZIP on disk.
await configManager.ensureLoaded();
const { features } = configManager.getPolicy();
if (!features.pluginsEnabled || !features.pluginsUploadEnabled || !features.requirePluginApproval) {
return NextResponse.json({ error: 'Plugin approval requests are turned off' }, { status: 403 });
}
// The request carries the package itself. The admin reviews the code
// and manifest the server read from it, and the approval is pinned to
// the hash the server computed, not to anything the client claims.