VYPR
High severity8.1NVD Advisory· Published May 6, 2026· Updated May 7, 2026

CVE-2026-43585

CVE-2026-43585

Description

OpenClaw before 2026.4.15 captures resolved bearer-auth configuration at startup, allowing revoked tokens to remain valid after SecretRef rotation. Gateway HTTP and WebSocket handlers fail to re-resolve authentication per-request, enabling attackers to use rotated-out bearer tokens for unauthorized gateway access.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.4.152026.4.15

Affected products

2
  • OpenClaw/Openclaw2 versions
    cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*+ 1 more
    • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*range: <2026.4.15
    • (no CPE)

Patches

1
acd4e0a32f12

fix(gateway): re-resolve HTTP auth per-request to honor credential rotation [AI] (#66651)

https://github.com/openclaw/openclawMichael AppelApr 14, 2026via ghsa
6 files changed · +104 2
  • CHANGELOG.md+1 0 modified
    @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
     - Setup/providers: guard preferred-provider lookup during setup so malformed plugin metadata with a missing provider id no longer crashes the wizard with `Cannot read properties of undefined (reading 'trim')`. (#66649) Thanks @Tianworld.
     - Matrix/security: normalize sandboxed profile avatar params, preserve `mxc://` avatar URLs, and surface gmail watcher stop failures during reload. (#64701) Thanks @slepybear.
     - Telegram/documents: drop leaked binary caption bytes from inbound Telegram text handling so document uploads like `.mobi` or `.epub` no longer explode prompt token counts. (#66663) Thanks @joelnishanth.
    +- Gateway/auth: resolve the active gateway bearer per-request on the HTTP server and the HTTP upgrade handler via `getResolvedAuth()`, mirroring the WebSocket path, so a secret rotated through `secrets.reload` or config hot-reload stops authenticating on `/v1/*`, `/tools/invoke`, plugin HTTP routes, and the canvas upgrade path immediately instead of remaining valid on HTTP until gateway restart. (#66651) Thanks @mmaps.
     
     ## 2026.4.14
     
    
  • src/gateway/server.canvas-auth.test.ts+34 2 modified
    @@ -123,9 +123,9 @@ async function expectWsRejected(
       });
     }
     
    -async function expectWsConnected(url: string): Promise<void> {
    +async function expectWsConnected(url: string, headers?: Record<string, string>): Promise<void> {
       await new Promise<void>((resolve, reject) => {
    -    const ws = new WebSocket(url);
    +    const ws = new WebSocket(url, headers ? { headers } : undefined);
         let settled = false;
         const finish = (fn: () => void) => {
           if (settled) {
    @@ -207,6 +207,7 @@ const allowCanvasHostHttp: CanvasHostHandler["handleHttpRequest"] = async (req,
     };
     async function withCanvasGatewayHarness(params: {
       resolvedAuth: ResolvedGatewayAuth;
    +  getResolvedAuth?: () => ResolvedGatewayAuth;
       listenHost?: string;
       rateLimiter?: ReturnType<typeof createAuthRateLimiter>;
       handleHttpRequest: CanvasHostHandler["handleHttpRequest"];
    @@ -241,6 +242,7 @@ async function withCanvasGatewayHarness(params: {
         openResponsesEnabled: false,
         handleHooksRequest: async () => false,
         resolvedAuth: params.resolvedAuth,
    +    getResolvedAuth: params.getResolvedAuth,
         rateLimiter: params.rateLimiter,
       });
     
    @@ -252,6 +254,7 @@ async function withCanvasGatewayHarness(params: {
         clients,
         preauthConnectionBudget: createPreauthConnectionBudget(8),
         resolvedAuth: params.resolvedAuth,
    +    getResolvedAuth: params.getResolvedAuth,
         rateLimiter: params.rateLimiter,
       });
     
    @@ -424,6 +427,35 @@ describe("gateway canvas host auth", () => {
         });
       }, 60_000);
     
    +  test("re-resolves canvas bearer auth on each upgrade after shared auth rotation", async () => {
    +    let currentAuth = tokenResolvedAuth;
    +
    +    await withCanvasGatewayHarness({
    +      resolvedAuth: tokenResolvedAuth,
    +      getResolvedAuth: () => currentAuth,
    +      handleHttpRequest: allowCanvasHostHttp,
    +      run: async ({ listener }) => {
    +        const url = `ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`;
    +
    +        await expectWsConnected(url, {
    +          authorization: "Bearer test-token",
    +        });
    +
    +        currentAuth = {
    +          ...tokenResolvedAuth,
    +          token: "rotated-token",
    +        };
    +
    +        await expectWsRejected(url, {
    +          authorization: "Bearer test-token",
    +        });
    +        await expectWsConnected(url, {
    +          authorization: "Bearer rotated-token",
    +        });
    +      },
    +    });
    +  }, 60_000);
    +
       test("accepts capability-scoped paths over IPv6 loopback", async () => {
         await withTempConfig({
           cfg: {
    
  • src/gateway/server-http.probe.test.ts+59 0 modified
    @@ -113,6 +113,65 @@ describe("gateway probe endpoints", () => {
         });
       });
     
    +  it("re-resolves auth for remote /ready requests after shared auth rotation", async () => {
    +    const getReadiness: ReadinessChecker = () => ({
    +      ready: false,
    +      failing: ["discord", "telegram"],
    +      uptimeMs: 8_000,
    +    });
    +    let currentAuth = AUTH_TOKEN;
    +
    +    await withGatewayServer({
    +      prefix: "probe-remote-rotated-auth",
    +      // `resolvedAuth` remains the static fallback; `getResolvedAuth` drives the rotated value.
    +      resolvedAuth: AUTH_TOKEN,
    +      overrides: {
    +        getReadiness,
    +        getResolvedAuth: () => currentAuth,
    +      },
    +      run: async (server) => {
    +        const sendReady = async (authorization: string) => {
    +          const req = createRequest({
    +            path: "/ready",
    +            remoteAddress: "10.0.0.8",
    +            host: "gateway.test",
    +            authorization,
    +          });
    +          const { res, getBody } = createResponse();
    +          await dispatchRequest(server, req, res);
    +          return { statusCode: res.statusCode, body: JSON.parse(getBody()) };
    +        };
    +
    +        await expect(sendReady("Bearer test-token")).resolves.toEqual({
    +          statusCode: 503,
    +          body: {
    +            ready: false,
    +            failing: ["discord", "telegram"],
    +            uptimeMs: 8_000,
    +          },
    +        });
    +
    +        currentAuth = {
    +          ...AUTH_TOKEN,
    +          token: "rotated-token",
    +        };
    +
    +        await expect(sendReady("Bearer test-token")).resolves.toEqual({
    +          statusCode: 503,
    +          body: { ready: false },
    +        });
    +        await expect(sendReady("Bearer rotated-token")).resolves.toEqual({
    +          statusCode: 503,
    +          body: {
    +            ready: false,
    +            failing: ["discord", "telegram"],
    +            uptimeMs: 8_000,
    +          },
    +        });
    +      },
    +    });
    +  });
    +
       it("hides readiness details when trusted-proxy auth violates browser origin policy", async () => {
         const getReadiness: ReadinessChecker = () => ({
           ready: false,
    
  • src/gateway/server-http.ts+6 0 modified
    @@ -838,6 +838,7 @@ export function createGatewayHttpServer(opts: {
       handlePluginRequest?: PluginHttpRequestHandler;
       shouldEnforcePluginGatewayAuth?: (pathContext: PluginRoutePathContext) => boolean;
       resolvedAuth: ResolvedGatewayAuth;
    +  getResolvedAuth?: () => ResolvedGatewayAuth;
       /** Optional rate limiter for auth brute-force protection. */
       rateLimiter?: AuthRateLimiter;
       getReadiness?: ReadinessChecker;
    @@ -861,6 +862,7 @@ export function createGatewayHttpServer(opts: {
         rateLimiter,
         getReadiness,
       } = opts;
    +  const getResolvedAuth = opts.getResolvedAuth ?? (() => resolvedAuth);
       const openAiCompatEnabled = openAiChatCompletionsEnabled || openResponsesEnabled;
       const httpServer: HttpServer = opts.tlsOptions
         ? createHttpsServer(opts.tlsOptions, (req, res) => {
    @@ -896,6 +898,7 @@ export function createGatewayHttpServer(opts: {
           const pluginPathContext = handlePluginRequest
             ? resolvePluginRoutePathContext(requestPath)
             : null;
    +      const resolvedAuth = getResolvedAuth();
           const requestStages: GatewayHttpRequestStage[] = [
             {
               name: "hooks",
    @@ -1117,6 +1120,7 @@ export function attachGatewayUpgradeHandler(opts: {
       clients: Set<GatewayWsClient>;
       preauthConnectionBudget: PreauthConnectionBudget;
       resolvedAuth: ResolvedGatewayAuth;
    +  getResolvedAuth?: () => ResolvedGatewayAuth;
       /** Optional rate limiter for auth brute-force protection. */
       rateLimiter?: AuthRateLimiter;
     }) {
    @@ -1129,6 +1133,7 @@ export function attachGatewayUpgradeHandler(opts: {
         resolvedAuth,
         rateLimiter,
       } = opts;
    +  const getResolvedAuth = opts.getResolvedAuth ?? (() => resolvedAuth);
       httpServer.on("upgrade", (req, socket, head) => {
         void (async () => {
           const configSnapshot = loadConfig();
    @@ -1143,6 +1148,7 @@ export function attachGatewayUpgradeHandler(opts: {
           if (scopedCanvas.rewrittenUrl) {
             req.url = scopedCanvas.rewrittenUrl;
           }
    +      const resolvedAuth = getResolvedAuth();
           if (canvasHost) {
             const url = new URL(req.url ?? "/", "http://localhost");
             if (url.pathname === CANVAS_WS_PATH) {
    
  • src/gateway/server.impl.ts+1 0 modified
    @@ -450,6 +450,7 @@ export async function startGatewayServer(
         resolvedAuth,
         rateLimiter: authRateLimiter,
         gatewayTls,
    +    getResolvedAuth,
         hooksConfig: () => runtimeState?.hooksConfig ?? initialHooksConfig,
         getHookClientIpConfig: () => runtimeState?.hookClientIpConfig ?? initialHookClientIpConfig,
         pluginRegistry,
    
  • src/gateway/server-runtime-state.ts+3 0 modified
    @@ -61,6 +61,7 @@ export async function createGatewayRuntimeState(params: {
       openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
       strictTransportSecurityHeader?: string;
       resolvedAuth: ResolvedGatewayAuth;
    +  getResolvedAuth: () => ResolvedGatewayAuth;
       /** Optional rate limiter for auth brute-force protection. */
       rateLimiter?: AuthRateLimiter;
       gatewayTls?: GatewayTlsRuntime;
    @@ -185,6 +186,7 @@ export async function createGatewayRuntimeState(params: {
             handlePluginRequest,
             shouldEnforcePluginGatewayAuth,
             resolvedAuth: params.resolvedAuth,
    +        getResolvedAuth: params.getResolvedAuth,
             rateLimiter: params.rateLimiter,
             getReadiness: params.getReadiness,
             tlsOptions: params.gatewayTls?.enabled ? params.gatewayTls.tlsOptions : undefined,
    @@ -224,6 +226,7 @@ export async function createGatewayRuntimeState(params: {
             clients,
             preauthConnectionBudget,
             resolvedAuth: params.resolvedAuth,
    +        getResolvedAuth: params.getResolvedAuth,
             rateLimiter: params.rateLimiter,
           });
         }
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

6

News mentions

0

No linked articles in our index yet.