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.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.15 | 2026.4.15 |
Affected products
2Patches
1acd4e0a32f12fix(gateway): re-resolve HTTP auth per-request to honor credential rotation [AI] (#66651)
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- github.com/openclaw/openclaw/commit/acd4e0a32f12e1ad85f3130f63b42443ce90f094nvdPatchWEB
- github.com/advisories/GHSA-xmxx-7p24-h892ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-xmxx-7p24-h892nvdMitigationVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-43585ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-bearer-token-validation-bypass-via-stale-secretref-resolutionnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/pull/66651ghsaWEB
News mentions
0No linked articles in our index yet.