VYPR
High severityNVD Advisory· Published Mar 12, 2026· Updated Mar 13, 2026

OpenClaw: Untrusted web origins can obtain authenticated operator.admin access in trusted-proxy mode

CVE-2026-32302

Description

OpenClaw is a personal AI assistant. Prior to 2026.3.11, browser-originated WebSocket connections could bypass origin validation when gateway.auth.mode was set to trusted-proxy and the request arrived with proxy headers. A page served from an untrusted origin could connect through a trusted reverse proxy, inherit proxy-authenticated identity, and establish a privileged operator session. This vulnerability is fixed in 2026.3.11.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.112026.3.11

Affected products

1

Patches

1
ebed3bbde1a7

fix(gateway): enforce browser origin check regardless of proxy headers

https://github.com/openclaw/openclawRobin WaslanderMar 12, 2026via ghsa
3 files changed · +128 1
  • CHANGELOG.md+4 0 modified
    @@ -4,6 +4,10 @@ Docs: https://docs.openclaw.ai
     
     ## Unreleased
     
    +### Security
    +
    +- Gateway/WebSocket: enforce browser origin validation for all browser-originated connections regardless of whether proxy headers are present, closing a cross-site WebSocket hijacking path in `trusted-proxy` mode that could grant untrusted origins `operator.admin` access. (GHSA-5wcw-8jjv-m286)
    +
     ### Changes
     
     - Gateway/node pending work: add narrow in-memory pending-work queue primitives (`node.pending.enqueue` / `node.pending.drain`) and wake-helper reuse as a foundation for dormant-node work delivery. (#41409) Thanks @mbelinky.
    
  • src/gateway/server.auth.browser-hardening.test.ts+123 0 modified
    @@ -12,6 +12,7 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-cha
     import { buildDeviceAuthPayload } from "./device-auth.js";
     import {
       connectReq,
    +  connectOk,
       installGatewayTestHooks,
       readConnectChallengeNonce,
       testState,
    @@ -27,6 +28,7 @@ const TEST_OPERATOR_CLIENT = {
       platform: "test",
       mode: GATEWAY_CLIENT_MODES.TEST,
     };
    +const ALLOWED_BROWSER_ORIGIN = "https://control.example.com";
     
     const originForPort = (port: number) => `http://127.0.0.1:${port}`;
     
    @@ -73,6 +75,127 @@ async function createSignedDevice(params: {
     }
     
     describe("gateway auth browser hardening", () => {
    +  test("rejects trusted-proxy browser connects from origins outside the allowlist", async () => {
    +    const { writeConfigFile } = await import("../config/config.js");
    +    await writeConfigFile({
    +      gateway: {
    +        auth: {
    +          mode: "trusted-proxy",
    +          trustedProxy: {
    +            userHeader: "x-forwarded-user",
    +            requiredHeaders: ["x-forwarded-proto"],
    +          },
    +        },
    +        trustedProxies: ["127.0.0.1"],
    +        controlUi: {
    +          allowedOrigins: [ALLOWED_BROWSER_ORIGIN],
    +        },
    +      },
    +    });
    +
    +    await withGatewayServer(async ({ port }) => {
    +      const ws = await openWs(port, {
    +        origin: "https://evil.example",
    +        "x-forwarded-for": "203.0.113.50",
    +        "x-forwarded-proto": "https",
    +        "x-forwarded-user": "operator@example.com",
    +      });
    +      try {
    +        const res = await connectReq(ws, {
    +          client: TEST_OPERATOR_CLIENT,
    +          device: null,
    +        });
    +        expect(res.ok).toBe(false);
    +        expect(res.error?.message ?? "").toContain("origin not allowed");
    +      } finally {
    +        ws.close();
    +      }
    +    });
    +  });
    +
    +  test("accepts trusted-proxy browser connects from allowed origins", async () => {
    +    const { writeConfigFile } = await import("../config/config.js");
    +    await writeConfigFile({
    +      gateway: {
    +        auth: {
    +          mode: "trusted-proxy",
    +          trustedProxy: {
    +            userHeader: "x-forwarded-user",
    +            requiredHeaders: ["x-forwarded-proto"],
    +          },
    +        },
    +        trustedProxies: ["127.0.0.1"],
    +        controlUi: {
    +          allowedOrigins: [ALLOWED_BROWSER_ORIGIN],
    +        },
    +      },
    +    });
    +
    +    await withGatewayServer(async ({ port }) => {
    +      const ws = await openWs(port, {
    +        origin: ALLOWED_BROWSER_ORIGIN,
    +        "x-forwarded-for": "203.0.113.50",
    +        "x-forwarded-proto": "https",
    +        "x-forwarded-user": "operator@example.com",
    +      });
    +      try {
    +        const payload = await connectOk(ws, {
    +          client: TEST_OPERATOR_CLIENT,
    +          device: null,
    +        });
    +        expect(payload.type).toBe("hello-ok");
    +      } finally {
    +        ws.close();
    +      }
    +    });
    +  });
    +
    +  test.each([
    +    {
    +      name: "rejects disallowed origins",
    +      origin: "https://evil.example",
    +      ok: false,
    +      expectedMessage: "origin not allowed",
    +    },
    +    {
    +      name: "accepts allowed origins",
    +      origin: ALLOWED_BROWSER_ORIGIN,
    +      ok: true,
    +    },
    +  ])(
    +    "keeps non-proxy browser-origin behavior unchanged: $name",
    +    async ({ origin, ok, expectedMessage }) => {
    +      const { writeConfigFile } = await import("../config/config.js");
    +      testState.gatewayAuth = { mode: "token", token: "secret" };
    +      await writeConfigFile({
    +        gateway: {
    +          controlUi: {
    +            allowedOrigins: [ALLOWED_BROWSER_ORIGIN],
    +          },
    +        },
    +      });
    +
    +      await withGatewayServer(async ({ port }) => {
    +        const ws = await openWs(port, { origin });
    +        try {
    +          const res = await connectReq(ws, {
    +            token: "secret",
    +            client: TEST_OPERATOR_CLIENT,
    +            device: null,
    +          });
    +          expect(res.ok).toBe(ok);
    +          if (ok) {
    +            expect((res.payload as { type?: string } | undefined)?.type).toBe("hello-ok");
    +          } else {
    +            expect(res.error?.message ?? "").toContain(expectedMessage ?? "");
    +          }
    +        } finally {
    +          ws.close();
    +        }
    +      });
    +    },
    +  );
    +
       test("rejects non-local browser origins for non-control-ui clients", async () => {
         testState.gatewayAuth = { mode: "token", token: "secret" };
         await withGatewayServer(async ({ port }) => {
    
  • src/gateway/server/ws-connection/message-handler.ts+1 1 modified
    @@ -114,7 +114,7 @@ function resolveHandshakeBrowserSecurityContext(params: {
       );
       return {
         hasBrowserOriginHeader,
    -    enforceOriginCheckForAnyClient: hasBrowserOriginHeader && !params.hasProxyHeaders,
    +    enforceOriginCheckForAnyClient: hasBrowserOriginHeader,
         rateLimitClientIp:
           hasBrowserOriginHeader && isLoopbackAddress(params.clientIp)
             ? BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP
    

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

5

News mentions

0

No linked articles in our index yet.