VYPR
High severity8.8NVD Advisory· Published Apr 10, 2026· Updated Apr 13, 2026

CVE-2026-35669

CVE-2026-35669

Description

OpenClaw before 2026.3.25 contains a privilege escalation vulnerability in gateway-authenticated plugin HTTP routes that incorrectly mint operator.admin runtime scope regardless of caller-granted scopes. Attackers can exploit this scope boundary bypass to gain elevated privileges and perform unauthorized administrative actions.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
<= 2026.3.24

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.3.25

Patches

1
ec2dbcff9afd

fix: keep plugin HTTP runtime scopes least-privileged (#55284)

https://github.com/openclaw/openclawJacob TomlinsonMar 26, 2026via ghsa
2 files changed · +55 15
  • src/gateway/server/plugins-http.test.ts+49 0 modified
    @@ -192,6 +192,55 @@ describe("createGatewayPluginRequestHandler", () => {
         expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("missing scope: operator.admin"));
       });
     
    +  it("keeps gateway-authenticated plugin routes on least-privilege runtime scopes", async () => {
    +    loadOpenClawPlugins.mockReset();
    +    handleGatewayRequest.mockReset();
    +    handleGatewayRequest.mockImplementation(async (opts: HandleGatewayRequestOptions) => {
    +      const scopes = opts.client?.connect.scopes ?? [];
    +      if (opts.req.method === "sessions.delete" && !scopes.includes("operator.admin")) {
    +        opts.respond(false, undefined, {
    +          code: "invalid_request",
    +          message: "missing scope: operator.admin",
    +        });
    +        return;
    +      }
    +      opts.respond(true, {});
    +    });
    +
    +    const subagent = await createSubagentRuntime();
    +    const log = createPluginLog();
    +    const handler = createGatewayPluginRequestHandler({
    +      registry: createTestRegistry({
    +        httpRoutes: [
    +          createRoute({
    +            path: "/secure-hook",
    +            auth: "gateway",
    +            handler: async (_req, _res) => {
    +              await subagent.deleteSession({ sessionKey: "agent:main:subagent:child" });
    +              return true;
    +            },
    +          }),
    +        ],
    +      }),
    +      log,
    +    });
    +
    +    const { res, setHeader, end } = makeMockHttpResponse();
    +    const handled = await handler({ url: "/secure-hook" } as IncomingMessage, res, undefined, {
    +      gatewayAuthSatisfied: true,
    +    });
    +
    +    expect(handled).toBe(true);
    +    expect(handleGatewayRequest).toHaveBeenCalledTimes(1);
    +    expect(handleGatewayRequest.mock.calls[0]?.[0]?.client?.connect.scopes).toEqual([
    +      "operator.write",
    +    ]);
    +    expect(res.statusCode).toBe(500);
    +    expect(setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
    +    expect(end).toHaveBeenCalledWith("Internal Server Error");
    +    expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("missing scope: operator.admin"));
    +  });
    +
       it("returns false when no routes are registered", async () => {
         const log = createPluginLog();
         const handler = createGatewayPluginRequestHandler({
    
  • src/gateway/server/plugins-http.ts+6 15 modified
    @@ -3,7 +3,7 @@ import type { createSubsystemLogger } from "../../logging/subsystem.js";
     import type { PluginRegistry } from "../../plugins/registry.js";
     import { resolveActivePluginHttpRouteRegistry } from "../../plugins/runtime.js";
     import { withPluginRuntimeGatewayRequestScope } from "../../plugins/runtime/gateway-request-scope.js";
    -import { ADMIN_SCOPE, APPROVALS_SCOPE, PAIRING_SCOPE, WRITE_SCOPE } from "../method-scopes.js";
    +import { WRITE_SCOPE } from "../method-scopes.js";
     import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../protocol/client-info.js";
     import { PROTOCOL_VERSION } from "../protocol/index.js";
     import type { GatewayRequestOptions } from "../server-methods/types.js";
    @@ -27,16 +27,10 @@ export { shouldEnforceGatewayAuthForPluginPath } from "./plugins-http/route-auth
     
     type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
     
    -function createPluginRouteRuntimeClient(params: {
    -  requiresGatewayAuth: boolean;
    -  gatewayAuthSatisfied?: boolean;
    -}): GatewayRequestOptions["client"] {
    -  // Plugin-authenticated webhooks can still use non-admin subagent helpers,
    -  // but they must not inherit admin-only gateway methods by default.
    -  const scopes =
    -    params.requiresGatewayAuth && params.gatewayAuthSatisfied !== false
    -      ? [ADMIN_SCOPE, APPROVALS_SCOPE, PAIRING_SCOPE]
    -      : [WRITE_SCOPE];
    +function createPluginRouteRuntimeClient(): GatewayRequestOptions["client"] {
    +  // Plugin HTTP handlers only need the least-privilege runtime scope.
    +  // Gateway route auth controls request admission, not runtime admin elevation.
    +  const scopes = [WRITE_SCOPE];
       return {
         connect: {
           minProtocol: PROTOCOL_VERSION,
    @@ -87,10 +81,7 @@ export function createGatewayPluginRequestHandler(params: {
           log.warn(`plugin http route blocked without gateway auth (${pathContext.canonicalPath})`);
           return false;
         }
    -    const runtimeClient = createPluginRouteRuntimeClient({
    -      requiresGatewayAuth,
    -      gatewayAuthSatisfied: dispatchContext?.gatewayAuthSatisfied,
    -    });
    +    const runtimeClient = createPluginRouteRuntimeClient();
     
         return await withPluginRuntimeGatewayRequestScope(
           {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.