Medium severity6.5NVD Advisory· Published Apr 10, 2026· Updated Apr 13, 2026
CVE-2026-35657
CVE-2026-35657
Description
OpenClaw before 2026.3.25 contains an authorization bypass vulnerability in the HTTP /sessions/:sessionKey/history route that skips operator.read scope validation. Attackers can access session history without proper operator read permissions by sending HTTP requests to the vulnerable endpoint.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.25 | 2026.3.25 |
Affected products
1Patches
11c4512323151Gateway: align HTTP session history scopes (#55285)
3 files changed · +101 −8
src/gateway/http-auth-helpers.ts+14 −1 modified@@ -2,7 +2,9 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { AuthRateLimiter } from "./auth-rate-limit.js"; import { authorizeHttpGatewayConnect, type ResolvedGatewayAuth } from "./auth.js"; import { sendGatewayAuthFailure } from "./http-common.js"; -import { getBearerToken } from "./http-utils.js"; +import { getBearerToken, getHeader } from "./http-utils.js"; + +const OPERATOR_SCOPES_HEADER = "x-openclaw-scopes"; export async function authorizeGatewayBearerRequestOrReply(params: { req: IncomingMessage; @@ -27,3 +29,14 @@ export async function authorizeGatewayBearerRequestOrReply(params: { } return true; } + +export function resolveGatewayRequestedOperatorScopes(req: IncomingMessage): string[] { + const raw = getHeader(req, OPERATOR_SCOPES_HEADER)?.trim(); + if (!raw) { + return []; + } + return raw + .split(",") + .map((scope) => scope.trim()) + .filter((scope) => scope.length > 0); +}
src/gateway/sessions-history-http.test.ts+67 −5 modified@@ -5,14 +5,17 @@ import { afterEach, describe, expect, test } from "vitest"; import { appendAssistantMessageToSessionTranscript } from "../config/sessions/transcript.js"; import { testState } from "./test-helpers.mocks.js"; import { + connectReq, createGatewaySuiteHarness, installGatewayTestHooks, + rpcReq, writeSessionStore, } from "./test-helpers.server.js"; installGatewayTestHooks(); const AUTH_HEADER = { Authorization: "Bearer test-gateway-token-1234567890" }; +const READ_SCOPE_HEADER = { "x-openclaw-scopes": "operator.read" }; const cleanupDirs: string[] = []; afterEach(async () => { @@ -93,7 +96,7 @@ describe("session history HTTP endpoints", () => { const res = await fetch( `http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:main")}/history`, { - headers: AUTH_HEADER, + headers: { ...AUTH_HEADER, ...READ_SCOPE_HEADER }, }, ); @@ -127,7 +130,7 @@ describe("session history HTTP endpoints", () => { const res = await fetch( `http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:missing")}/history`, { - headers: AUTH_HEADER, + headers: { ...AUTH_HEADER, ...READ_SCOPE_HEADER }, }, ); @@ -195,7 +198,7 @@ describe("session history HTTP endpoints", () => { const res = await fetch( `http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:main")}/history`, { - headers: AUTH_HEADER, + headers: { ...AUTH_HEADER, ...READ_SCOPE_HEADER }, }, ); @@ -231,7 +234,7 @@ describe("session history HTTP endpoints", () => { const firstPage = await fetch( `http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:main")}/history?limit=2`, { - headers: AUTH_HEADER, + headers: { ...AUTH_HEADER, ...READ_SCOPE_HEADER }, }, ); expect(firstPage.status).toBe(200); @@ -254,7 +257,7 @@ describe("session history HTTP endpoints", () => { const secondPage = await fetch( `http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:main")}/history?limit=2&cursor=${encodeURIComponent(firstBody.nextCursor ?? "")}`, { - headers: AUTH_HEADER, + headers: { ...AUTH_HEADER, ...READ_SCOPE_HEADER }, }, ); expect(secondPage.status).toBe(200); @@ -291,6 +294,7 @@ describe("session history HTTP endpoints", () => { { headers: { ...AUTH_HEADER, + ...READ_SCOPE_HEADER, Accept: "text/event-stream", }, }, @@ -337,6 +341,7 @@ describe("session history HTTP endpoints", () => { { headers: { ...AUTH_HEADER, + ...READ_SCOPE_HEADER, Accept: "text/event-stream", }, }, @@ -395,4 +400,61 @@ describe("session history HTTP endpoints", () => { await harness.close(); } }); + + test("rejects session history when operator.read is not requested", async () => { + await seedSession({ text: "scope-guarded history" }); + + const harness = await createGatewaySuiteHarness(); + const ws = await harness.openWs(); + try { + const connect = await connectReq(ws, { + token: "test-gateway-token-1234567890", + scopes: ["operator.approvals"], + }); + expect(connect.ok).toBe(true); + + const wsHistory = await rpcReq<{ messages?: unknown[] }>(ws, "chat.history", { + sessionKey: "agent:main:main", + limit: 1, + }); + expect(wsHistory.ok).toBe(false); + expect(wsHistory.error?.message).toBe("missing scope: operator.read"); + + const httpHistory = await fetch( + `http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:main")}/history?limit=1`, + { + headers: { + ...AUTH_HEADER, + "x-openclaw-scopes": "operator.approvals", + }, + }, + ); + expect(httpHistory.status).toBe(403); + await expect(httpHistory.json()).resolves.toMatchObject({ + ok: false, + error: { + type: "forbidden", + message: "missing scope: operator.read", + }, + }); + + const httpHistoryWithoutScopes = await fetch( + `http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:main")}/history?limit=1`, + { + headers: AUTH_HEADER, + }, + ); + expect(httpHistoryWithoutScopes.status).toBe(403); + await expect(httpHistoryWithoutScopes.json()).resolves.toMatchObject({ + ok: false, + error: { + type: "forbidden", + message: "missing scope: operator.read", + }, + }); + } finally { + ws.close(); + await harness.close(); + } + }); });
src/gateway/sessions-history-http.ts+20 −2 modified@@ -6,14 +6,18 @@ import { loadSessionStore } from "../config/sessions.js"; import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js"; import type { AuthRateLimiter } from "./auth-rate-limit.js"; import type { ResolvedGatewayAuth } from "./auth.js"; -import { authorizeGatewayBearerRequestOrReply } from "./http-auth-helpers.js"; +import { + authorizeGatewayBearerRequestOrReply, + resolveGatewayRequestedOperatorScopes, +} from "./http-auth-helpers.js"; import { sendInvalidRequest, sendJson, sendMethodNotAllowed, setSseHeaders, } from "./http-common.js"; import { getHeader } from "./http-utils.js"; +import { authorizeOperatorScopesForMethod } from "./method-scopes.js"; import { attachOpenClawTranscriptMeta, readSessionMessages, @@ -23,7 +27,6 @@ import { } from "./session-utils.js"; const MAX_SESSION_HISTORY_LIMIT = 1000; - function resolveSessionHistoryPath(req: IncomingMessage): string | null { const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); const match = url.pathname.match(/^\/sessions\/([^/]+)\/history$/); @@ -167,6 +170,21 @@ export async function handleSessionHistoryHttpRequest( return true; } + // HTTP callers must declare the same least-privilege operator scopes they + // intend to use over WS so both transport surfaces enforce the same gate. + const requestedScopes = resolveGatewayRequestedOperatorScopes(req); + const scopeAuth = authorizeOperatorScopesForMethod("chat.history", requestedScopes); + if (!scopeAuth.allowed) { + sendJson(res, 403, { + ok: false, + error: { + type: "forbidden", + message: `missing scope: ${scopeAuth.missingScope}`, + }, + }); + return true; + } + const target = resolveGatewaySessionStoreTarget({ cfg, key: sessionKey }); const store = loadSessionStore(target.storePath); const entry = resolveFreshestSessionEntryFromStoreKeys(store, target.storeKeys);
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- github.com/openclaw/openclaw/commit/1c45123231516fa50f8cf8522ba5ff2fb2ca7aeanvdPatchWEB
- github.com/advisories/GHSA-5jvj-hxmh-6h6jghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-5jvj-hxmh-6h6jnvdVendor AdvisoryWEB
- www.vulncheck.com/advisories/openclaw-authorization-bypass-in-http-session-history-routenvdThird Party Advisory
News mentions
0No linked articles in our index yet.