### 漏洞概述 该漏洞涉及在插件路由运行时作用域中未正确限制写操作权限,导致恶意插件可能通过伪造请求绕过权限检查,执行未授权的操作。 ### 影响范围 - **受影响组件**:`src/gateway/server/plugins-http-runtime-scopes.test.ts` 和 `src/gateway/server/plugins-http.test.ts` - **影响版本**:v2026.4.26 至 v2026.3.31 (beta.1) ### 修复方案 1. **代码修改**: - 在 `src/gateway/server/plugins-http-runtime-scopes.test.ts` 中,增加了 `createTestRegistry` 和 `createGatewayPluginRequestHandler` 函数,用于测试插件路由的运行时作用域。 - 在 `src/gateway/server/plugins-http.test.ts` 中,增加了 `createSubagentRuntimeRegistry` 和 `createSecurePluginRouteHandler` 函数,用于测试插件路由的安全性和作用域。 2. **具体代码**: ```typescript // src/gateway/server/plugins-http-runtime-scopes.test.ts import { IncomingMessage, ServerResponse } from "node:http"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { SubsystemLogger } from "../logging/subsystem.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { releasePinnedPluginsHttpRouteRegistry, setActivePluginRegistry, } from "../plugins/runtime.js"; import { getPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; import { authorizeOperatorScopesForMethod } from "../method-scopes.js"; import { makeMockHttpResponse } from "../test-http-response.js"; import { createTestRegistry } from "../_tests_/test-utils.js"; import { createGatewayPluginRequestHandler } from "../plugins-http.js"; function createMockParams({ path, auth, handler, }: { path: string; auth: "gateway" | "plugin"; handler?: (req: IncomingMessage, res: ServerResponse) => boolean | Promise; }) { return { pluginId: "route", path: params.path, auth: params.auth, match: "exact" as const, handler: params.handler ?? (() => true), source: "route", }; } function createMockLogger(): SubsystemLogger { const logger = { subsystem: "test/plugins-http-runtime-scopes", isDisabled: () => true, trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn(), raw: vi.fn(), child: vi.fn(), } satisfies Omit & { child: ReturnType }; logger.child.mockImplementation(() => logger); return logger; } function assertWriteScopeAllowed() { const scopes = getPluginRuntimeGatewayRequestScope().client?.connect?.scopes ?? []; const auth = authorizeOperatorScopesForMethod("agent", scopes); if (!auth.allowed) { throw new Error(`missing scope: ${auth.missingScope}`); } } describe("plugin HTTP route runtime scopes", () => { afterEach(() => { releasePinnedPluginsHttpRouteRegistry(); setActivePluginRegistry(createEmptyPluginRegistry()); }); async function invokeRoute(params: { path: string; auth: "gateway" | "plugin"; gatewayAuthSatisfied: boolean; }) { const log = createMockLogger(); const handler = createGatewayPluginRequestHandler({ registry: createTestRegistry({ httpRoutes: [ createRoute({ path: params.path, auth: params.auth, handler: async () => { assertWriteScopeAllowed(); return true; }, }), ], }), log, }); const response = makeMockHttpResponse(); const handled = await handler({ url: params.path, } as IncomingMessage, response.res, undefined, { gatewayAuthSatisfied: params.gatewayAuthSatisfied, }); return { handled, log, ...response }; } it("keeps plugin-auth routes off write-capable runtime helpers", async () => { const { handled, res, setStatus, end, log } = await invokeRoute({ path: "/hook", auth: "plugin", gatewayAuthSatisfied: false, }); expect(handled).toBe(true); expect(res.statusCode).toBe(500); expect(setStatus).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8"); expect(end).toHaveBeenCalledWith("Internal Server Error"); expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("missing scope: operator.write")); }); it("preserves write-capable runtime helpers on gateway-auth routes", async () => { const { handled, res, log } = await invokeRoute({ path: "/secure-hook", auth: "gateway", gatewayAuthSatisfied: true, }); expect(handled).toBe(true); expect(res.statusCode).toBe(200); expect(log.warn).not.toHaveBeenCalled(); }); it.each([ { auth: "plugin" as const, gatewayAuthSatisfied: false, path: "/hook", expectedScopes: [], }, { auth: "gateway" as const, gatewayAuthSatisfied: true, path: "/secure-hook", expectedScopes: ["operator.write"], }, ])("maps auth routes to expectedScopes", async ({ auth, gatewayAuthSatisfied, path, expectedScopes }) => { let observedScopes: string[] | undefined; const handler = createGatewayPluginRequestHandler({ registry: createTestRegistry({ httpRoutes: [ createRoute({ path, auth, handler: vi.fn(async () => { observedScopes = getPluginRuntimeGatewayRequestScope().client?.connect?.scopes?.slice() ?? []; return true; }), }), ], }), log: createMockLogger(), }); const { res } = makeMockHttpResponse(); const handled = await handler({ url: path } as IncomingMessage, res, undefined, { gatewayAuthSatisfied, }); expect(handled).toBe(true); expect(res.statusCode).toBe(200); expect(observedScopes).toEqual(expectedScopes); }); }); ``` ```typescript // src/gateway/server/plugins-http.test.ts import { releasePinnedPluginsHttpRouteRegistry, setActivePluginRe