Plugin subagent dispatch used a hardcoded synthetic client carrying operator.admin, operator.approvals, and operator.pairing for all runtime.subagent.* calls. Plugin HTTP routes with auth:"plugin" require no gateway auth by design, so an unauthenticated external request could drive admin-only gateway methods (sessions.delete, agent.run) through the subagent runtime. Propagate the real gateway client into the plugin runtime request scope when one is available. Plugin HTTP routes now run inside a scoped runtime client: auth:"plugin" routes receive a non-admin synthetic operator.write client; gateway-authenticated routes retain admin-capable scopes. The security boundary is enforced at the HTTP handler level. Fixes GHSA-xw77-45gv-p728
48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
import { AsyncLocalStorage } from "node:async_hooks";
|
|
import type {
|
|
GatewayRequestContext,
|
|
GatewayRequestOptions,
|
|
} from "../../gateway/server-methods/types.js";
|
|
|
|
export type PluginRuntimeGatewayRequestScope = {
|
|
context?: GatewayRequestContext;
|
|
client?: GatewayRequestOptions["client"];
|
|
isWebchatConnect: GatewayRequestOptions["isWebchatConnect"];
|
|
};
|
|
|
|
const PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY: unique symbol = Symbol.for(
|
|
"openclaw.pluginRuntimeGatewayRequestScope",
|
|
);
|
|
|
|
const pluginRuntimeGatewayRequestScope = (() => {
|
|
const globalState = globalThis as typeof globalThis & {
|
|
[PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY]?: AsyncLocalStorage<PluginRuntimeGatewayRequestScope>;
|
|
};
|
|
const existing = globalState[PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY];
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
const created = new AsyncLocalStorage<PluginRuntimeGatewayRequestScope>();
|
|
globalState[PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY] = created;
|
|
return created;
|
|
})();
|
|
|
|
/**
|
|
* Runs plugin gateway handlers with request-scoped context that runtime helpers can read.
|
|
*/
|
|
export function withPluginRuntimeGatewayRequestScope<T>(
|
|
scope: PluginRuntimeGatewayRequestScope,
|
|
run: () => T,
|
|
): T {
|
|
return pluginRuntimeGatewayRequestScope.run(scope, run);
|
|
}
|
|
|
|
/**
|
|
* Returns the current plugin gateway request scope when called from a plugin request handler.
|
|
*/
|
|
export function getPluginRuntimeGatewayRequestScope():
|
|
| PluginRuntimeGatewayRequestScope
|
|
| undefined {
|
|
return pluginRuntimeGatewayRequestScope.getStore();
|
|
}
|